Skip to main content

nwnrs_set/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = "# nwnrs-set\n\nTyped parser for Neverwinter Nights tileset (`SET`) payloads.\n\n## Scope\n\n- parse the INI-like tileset structure into typed sections\n- build deterministic `SET` text from the typed representation\n- write typed tilesets back to a stream\n- model tiles, terrain tags, crosser tags, groups, grass settings, and tile\n  door metadata explicitly\n- expose the authored tileset catalog without coupling it to a renderer\n\nThe primary entry points are [`read_set`], [`build_set_text`], [`write_set`],\nand [`SetFile`].\n\n## Public Surface\n\n- `SET_RES_TYPE`\n- `SetError`\n- `SetResult`\n- `SetFile`\n- `SetGeneral`\n- `SetGrass`\n- `SetNamedType`\n- `SetPrimaryRule`\n- `SetTile`\n- `SetTileCorner`\n- `SetTileEdges`\n- `SetTileDoor`\n- `SetGroup`\n- `read_set`\n- `parse_set`\n- `build_set_text`\n- `write_set`\n\n## Core Model\n\n`SetFile` preserves distinct keyed collections for:\n\n- `general`\n- optional `grass`\n- `terrains`\n- `crossers`\n- `primary_rules`\n- `tiles`\n- `tile_doors`\n- `groups`\n\nImportant typed pieces:\n\n- `SetTileCorner`\n  - terrain tag\n  - height step\n- `SetTileEdges`\n  - explicit top, right, bottom, and left crosser tags\n- `SetTile`\n  - model reference\n  - walkmesh reference\n  - terrain annotations\n  - lighting and animation flags\n  - tile-level visibility and pathing metadata\n\n## Text Layout\n\n`SET` is INI-like and section-oriented.\n\n```text\n[GENERAL]\n...\n\n[GRASS]\n...\n\n[TERRAIN0]\n...\n\n[CROSSER0]\n...\n\n[PRIMARY RULE0]\n...\n\n[TILE0]\n...\n\n[TILE0DOOR0]\n...\n\n[GROUP0]\n...\n```\n\nConceptually:\n\n```text\n+----------------------+\n| global metadata      |\n+----------------------+\n| optional grass block |\n+----------------------+\n| terrain catalog      |\n+----------------------+\n| crosser catalog      |\n+----------------------+\n| rule catalog         |\n+----------------------+\n| tile catalog         |\n+----------------------+\n| tile-door metadata   |\n+----------------------+\n| groups               |\n+----------------------+\n```\n\n## Invariants\n\n- section identity is preserved explicitly through typed collections keyed by\n  their authored ids\n- tile, group, terrain, crosser, and door metadata remain distinct rather than\n  being merged into one generic map\n- optional values remain optional rather than being normalized to arbitrary\n  defaults\n- deterministic serialization rebuilds the modeled section structure in\n  ascending key order\n\n## See also\n\n- [`nwnrs-git`](https://docs.rs/nwnrs-git), which models the area instance data\n  that references tileset resources\n- [`nwnrs-mdl`](https://docs.rs/nwnrs-mdl), which handles the model assets that\n  tileset tile entries point to\n\n## Why This Crate Exists\n\n`SET` is one of the clearest examples in the workspace of \"catalog structure is\ndata.\" If you flatten it into one generic section map, you lose too much:\n\n- explicit typed tile semantics\n- tile-door relationship structure\n- terrain and crosser taxonomy\n- deterministic reconstruction of the authored tileset catalog\n"include_str!("../README.md")]
3
4use std::{
5    collections::BTreeMap,
6    fmt,
7    fs::File,
8    io::{self, Read, Write},
9    path::Path,
10};
11
12use nwnrs_resman::prelude::*;
13use nwnrs_resref::prelude::ResolvedResRef;
14use nwnrs_restype::prelude::*;
15use tracing::instrument;
16
17/// NWN resource type id for `set`.
18pub const SET_RES_TYPE: ResType = ResType(2013);
19
20/// Errors returned while reading or parsing `SET` payloads.
21///
22/// # Examples
23///
24/// ```rust,no_run
25/// let _ = std::mem::size_of::<nwnrs_set::SetError>();
26/// ```
27#[derive(#[automatically_derived]
impl ::core::fmt::Debug for SetError {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        match self {
            SetError::Io(__self_0) =>
                ::core::fmt::Formatter::debug_tuple_field1_finish(f, "Io",
                    &__self_0),
            SetError::ResMan(__self_0) =>
                ::core::fmt::Formatter::debug_tuple_field1_finish(f, "ResMan",
                    &__self_0),
            SetError::Message(__self_0) =>
                ::core::fmt::Formatter::debug_tuple_field1_finish(f,
                    "Message", &__self_0),
        }
    }
}Debug)]
28pub enum SetError {
29    /// An underlying IO operation failed.
30    Io(io::Error),
31    /// Resource-manager access failed.
32    ResMan(ResManError),
33    /// The payload was otherwise invalid or unsupported.
34    Message(String),
35}
36
37impl SetError {
38    /// Creates a free-form `SET` error message.
39    ///
40    /// # Examples
41    ///
42    /// ```rust,no_run
43    /// let _ = nwnrs_set::SetError::msg;
44    /// ```
45    pub fn msg(message: impl Into<String>) -> Self {
46        Self::Message(message.into())
47    }
48}
49
50impl fmt::Display for SetError {
51    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52        match self {
53            Self::Io(error) => error.fmt(f),
54            Self::ResMan(error) => error.fmt(f),
55            Self::Message(message) => f.write_str(message),
56        }
57    }
58}
59
60impl std::error::Error for SetError {}
61
62impl From<io::Error> for SetError {
63    fn from(value: io::Error) -> Self {
64        Self::Io(value)
65    }
66}
67
68impl From<ResManError> for SetError {
69    fn from(value: ResManError) -> Self {
70        Self::ResMan(value)
71    }
72}
73
74/// Result type for `SET` operations.
75pub type SetResult<T> = Result<T, SetError>;
76
77/// Parsed tileset payload.
78///
79/// The representation preserves the authored section structure explicitly:
80/// top-level metadata, terrain and crosser catalogs, primary rules, tiles,
81/// tile-door metadata, and groups remain distinct keyed collections rather than
82/// being flattened into one generic map.
83///
84/// # Examples
85///
86/// ```rust,no_run
87/// let set_file = nwnrs_set::SetFile::default();
88/// assert!(set_file.tiles.is_empty());
89/// ```
90#[derive(#[automatically_derived]
impl ::core::fmt::Debug for SetFile {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        let names: &'static _ =
            &["general", "grass", "terrains", "crossers", "primary_rules",
                        "tiles", "tile_doors", "groups"];
        let values: &[&dyn ::core::fmt::Debug] =
            &[&self.general, &self.grass, &self.terrains, &self.crossers,
                        &self.primary_rules, &self.tiles, &self.tile_doors,
                        &&self.groups];
        ::core::fmt::Formatter::debug_struct_fields_finish(f, "SetFile",
            names, values)
    }
}Debug, #[automatically_derived]
impl ::core::clone::Clone for SetFile {
    #[inline]
    fn clone(&self) -> SetFile {
        SetFile {
            general: ::core::clone::Clone::clone(&self.general),
            grass: ::core::clone::Clone::clone(&self.grass),
            terrains: ::core::clone::Clone::clone(&self.terrains),
            crossers: ::core::clone::Clone::clone(&self.crossers),
            primary_rules: ::core::clone::Clone::clone(&self.primary_rules),
            tiles: ::core::clone::Clone::clone(&self.tiles),
            tile_doors: ::core::clone::Clone::clone(&self.tile_doors),
            groups: ::core::clone::Clone::clone(&self.groups),
        }
    }
}Clone, #[automatically_derived]
impl ::core::default::Default for SetFile {
    #[inline]
    fn default() -> SetFile {
        SetFile {
            general: ::core::default::Default::default(),
            grass: ::core::default::Default::default(),
            terrains: ::core::default::Default::default(),
            crossers: ::core::default::Default::default(),
            primary_rules: ::core::default::Default::default(),
            tiles: ::core::default::Default::default(),
            tile_doors: ::core::default::Default::default(),
            groups: ::core::default::Default::default(),
        }
    }
}Default, #[automatically_derived]
impl ::core::cmp::PartialEq for SetFile {
    #[inline]
    fn eq(&self, other: &SetFile) -> bool {
        self.general == other.general && self.grass == other.grass &&
                                self.terrains == other.terrains &&
                            self.crossers == other.crossers &&
                        self.primary_rules == other.primary_rules &&
                    self.tiles == other.tiles &&
                self.tile_doors == other.tile_doors &&
            self.groups == other.groups
    }
}PartialEq)]
91pub struct SetFile {
92    /// Top-level `[GENERAL]` metadata.
93    pub general:       SetGeneral,
94    /// Optional `[GRASS]` block.
95    pub grass:         Option<SetGrass>,
96    /// `[TERRAINN]` entries keyed by terrain id.
97    pub terrains:      BTreeMap<u32, SetNamedType>,
98    /// `[CROSSERN]` entries keyed by crosser id.
99    pub crossers:      BTreeMap<u32, SetNamedType>,
100    /// `[PRIMARY RULEN]` entries keyed by rule id.
101    pub primary_rules: BTreeMap<u32, SetPrimaryRule>,
102    /// `[TILEN]` entries keyed by tile id.
103    pub tiles:         BTreeMap<u32, SetTile>,
104    /// `[TILENMDOORK]` entries keyed by `(tile_id, door_id)`.
105    pub tile_doors:    BTreeMap<(u32, u32), SetTileDoor>,
106    /// `[GROUPN]` entries keyed by group id.
107    pub groups:        BTreeMap<u32, SetGroup>,
108}
109
110impl SetFile {
111    /// Reads a typed `SET` file from disk.
112    ///
113    /// # Errors
114    ///
115    /// Returns [`SetError`] if the file cannot be opened or parsed.
116    ///
117    /// # Examples
118    ///
119    /// ```rust,no_run
120    /// let _ = nwnrs_set::SetFile::from_file;
121    /// ```
122    pub fn from_file(path: impl AsRef<Path>) -> SetResult<Self> {
123        let mut file = File::open(path.as_ref())?;
124        read_set(&mut file)
125    }
126
127    /// Reads a typed `SET` file from a [`Res`].
128    ///
129    /// # Errors
130    ///
131    /// Returns [`SetError`] if the resource is not a SET type or the bytes
132    /// cannot be parsed.
133    ///
134    /// # Examples
135    ///
136    /// ```rust,no_run
137    /// let _ = nwnrs_set::SetFile::from_res;
138    /// ```
139    pub fn from_res(res: &Res, cache_policy: CachePolicy) -> SetResult<Self> {
140        if res.resref().res_type() != SET_RES_TYPE {
141            return Err(SetError::msg(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("expected set resource, got {0}",
                res.resref()))
    })format!(
142                "expected set resource, got {}",
143                res.resref()
144            )));
145        }
146
147        let bytes = res.read_all(cache_policy)?;
148        let text = String::from_utf8(bytes)
149            .map_err(|error| SetError::msg(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("SET payload is not valid UTF-8: {0}",
                error))
    })format!("SET payload is not valid UTF-8: {error}")))?;
150        parse_set(&text)
151    }
152
153    /// Reads a typed `SET` file from a [`ResMan`] by tileset name.
154    ///
155    /// # Examples
156    ///
157    /// ```rust,no_run
158    /// let _ = nwnrs_set::SetFile::from_resman;
159    /// ```
160    pub fn from_resman(
161        resman: &mut ResMan,
162        set_name: &str,
163        cache_policy: CachePolicy,
164    ) -> SetResult<Self> {
165        let resolved = ResolvedResRef::from_filename(&::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0}.set", set_name))
    })format!("{set_name}.set"))
166            .map_err(|error| SetError::msg(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("set resref: {0}", error))
    })format!("set resref: {error}")))?;
167        let res = resman
168            .get_resolved(&resolved)
169            .ok_or_else(|| SetError::msg(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("tileset not found in ResMan: {0}",
                resolved))
    })format!("tileset not found in ResMan: {resolved}")))?;
170        Self::from_res(&res, cache_policy)
171    }
172}
173
174/// Parsed `[GENERAL]` section.
175///
176/// # Examples
177///
178/// ```rust,no_run
179/// let general = nwnrs_set::SetGeneral::default();
180/// assert!(general.name.is_none());
181/// ```
182#[derive(#[automatically_derived]
impl ::core::fmt::Debug for SetGeneral {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        let names: &'static _ =
            &["name", "file_type", "version", "interior",
                        "has_height_transition", "env_map", "transition",
                        "selector_height", "display_name", "unlocalized_name",
                        "border", "default_terrain", "floor"];
        let values: &[&dyn ::core::fmt::Debug] =
            &[&self.name, &self.file_type, &self.version, &self.interior,
                        &self.has_height_transition, &self.env_map,
                        &self.transition, &self.selector_height, &self.display_name,
                        &self.unlocalized_name, &self.border, &self.default_terrain,
                        &&self.floor];
        ::core::fmt::Formatter::debug_struct_fields_finish(f, "SetGeneral",
            names, values)
    }
}Debug, #[automatically_derived]
impl ::core::clone::Clone for SetGeneral {
    #[inline]
    fn clone(&self) -> SetGeneral {
        SetGeneral {
            name: ::core::clone::Clone::clone(&self.name),
            file_type: ::core::clone::Clone::clone(&self.file_type),
            version: ::core::clone::Clone::clone(&self.version),
            interior: ::core::clone::Clone::clone(&self.interior),
            has_height_transition: ::core::clone::Clone::clone(&self.has_height_transition),
            env_map: ::core::clone::Clone::clone(&self.env_map),
            transition: ::core::clone::Clone::clone(&self.transition),
            selector_height: ::core::clone::Clone::clone(&self.selector_height),
            display_name: ::core::clone::Clone::clone(&self.display_name),
            unlocalized_name: ::core::clone::Clone::clone(&self.unlocalized_name),
            border: ::core::clone::Clone::clone(&self.border),
            default_terrain: ::core::clone::Clone::clone(&self.default_terrain),
            floor: ::core::clone::Clone::clone(&self.floor),
        }
    }
}Clone, #[automatically_derived]
impl ::core::default::Default for SetGeneral {
    #[inline]
    fn default() -> SetGeneral {
        SetGeneral {
            name: ::core::default::Default::default(),
            file_type: ::core::default::Default::default(),
            version: ::core::default::Default::default(),
            interior: ::core::default::Default::default(),
            has_height_transition: ::core::default::Default::default(),
            env_map: ::core::default::Default::default(),
            transition: ::core::default::Default::default(),
            selector_height: ::core::default::Default::default(),
            display_name: ::core::default::Default::default(),
            unlocalized_name: ::core::default::Default::default(),
            border: ::core::default::Default::default(),
            default_terrain: ::core::default::Default::default(),
            floor: ::core::default::Default::default(),
        }
    }
}Default, #[automatically_derived]
impl ::core::cmp::PartialEq for SetGeneral {
    #[inline]
    fn eq(&self, other: &SetGeneral) -> bool {
        self.name == other.name && self.file_type == other.file_type &&
                                                    self.version == other.version &&
                                                self.interior == other.interior &&
                                            self.has_height_transition == other.has_height_transition &&
                                        self.env_map == other.env_map &&
                                    self.transition == other.transition &&
                                self.selector_height == other.selector_height &&
                            self.display_name == other.display_name &&
                        self.unlocalized_name == other.unlocalized_name &&
                    self.border == other.border &&
                self.default_terrain == other.default_terrain &&
            self.floor == other.floor
    }
}PartialEq)]
183pub struct SetGeneral {
184    /// Internal tileset name.
185    pub name:                  Option<String>,
186    /// Declared resource type, usually `SET`.
187    pub file_type:             Option<String>,
188    /// Declared version string.
189    pub version:               Option<String>,
190    /// Whether the tileset is interior.
191    pub interior:              Option<bool>,
192    /// Whether height transitions are enabled.
193    pub has_height_transition: Option<bool>,
194    /// Environment map name.
195    pub env_map:               Option<String>,
196    /// Transition type id.
197    pub transition:            Option<i32>,
198    /// Selector height hint.
199    pub selector_height:       Option<i32>,
200    /// Dialog.tlk string reference for the localized display name.
201    pub display_name:          Option<i32>,
202    /// Fallback unlocalized display name.
203    pub unlocalized_name:      Option<String>,
204    /// Default border terrain tag.
205    pub border:                Option<String>,
206    /// Default terrain tag.
207    pub default_terrain:       Option<String>,
208    /// Default floor terrain tag.
209    pub floor:                 Option<String>,
210}
211
212/// Parsed `[GRASS]` section.
213///
214/// # Examples
215///
216/// ```rust,no_run
217/// let grass = nwnrs_set::SetGrass::default();
218/// assert!(grass.texture_name.is_none());
219/// ```
220#[derive(#[automatically_derived]
impl ::core::fmt::Debug for SetGrass {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        let names: &'static _ =
            &["grass", "texture_name", "density", "height", "ambient",
                        "diffuse"];
        let values: &[&dyn ::core::fmt::Debug] =
            &[&self.grass, &self.texture_name, &self.density, &self.height,
                        &self.ambient, &&self.diffuse];
        ::core::fmt::Formatter::debug_struct_fields_finish(f, "SetGrass",
            names, values)
    }
}Debug, #[automatically_derived]
impl ::core::clone::Clone for SetGrass {
    #[inline]
    fn clone(&self) -> SetGrass {
        SetGrass {
            grass: ::core::clone::Clone::clone(&self.grass),
            texture_name: ::core::clone::Clone::clone(&self.texture_name),
            density: ::core::clone::Clone::clone(&self.density),
            height: ::core::clone::Clone::clone(&self.height),
            ambient: ::core::clone::Clone::clone(&self.ambient),
            diffuse: ::core::clone::Clone::clone(&self.diffuse),
        }
    }
}Clone, #[automatically_derived]
impl ::core::default::Default for SetGrass {
    #[inline]
    fn default() -> SetGrass {
        SetGrass {
            grass: ::core::default::Default::default(),
            texture_name: ::core::default::Default::default(),
            density: ::core::default::Default::default(),
            height: ::core::default::Default::default(),
            ambient: ::core::default::Default::default(),
            diffuse: ::core::default::Default::default(),
        }
    }
}Default, #[automatically_derived]
impl ::core::cmp::PartialEq for SetGrass {
    #[inline]
    fn eq(&self, other: &SetGrass) -> bool {
        self.grass == other.grass && self.texture_name == other.texture_name
                        && self.density == other.density &&
                    self.height == other.height && self.ambient == other.ambient
            && self.diffuse == other.diffuse
    }
}PartialEq)]
221pub struct SetGrass {
222    /// Whether grass rendering is enabled.
223    pub grass:        Option<bool>,
224    /// Grass texture resource name.
225    pub texture_name: Option<String>,
226    /// Grass density value.
227    pub density:      Option<f32>,
228    /// Grass height value.
229    pub height:       Option<f32>,
230    /// Ambient grass color.
231    pub ambient:      Option<[f32; 3]>,
232    /// Diffuse grass color.
233    pub diffuse:      Option<[f32; 3]>,
234}
235
236/// Named tileset catalog entry such as `[TERRAIN0]` or `[CROSSER0]`.
237///
238/// # Examples
239///
240/// ```rust,no_run
241/// let named = nwnrs_set::SetNamedType::default();
242/// assert_eq!(named.id, 0);
243/// ```
244#[derive(#[automatically_derived]
impl ::core::fmt::Debug for SetNamedType {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        ::core::fmt::Formatter::debug_struct_field3_finish(f, "SetNamedType",
            "id", &self.id, "name", &self.name, "str_ref", &&self.str_ref)
    }
}Debug, #[automatically_derived]
impl ::core::clone::Clone for SetNamedType {
    #[inline]
    fn clone(&self) -> SetNamedType {
        SetNamedType {
            id: ::core::clone::Clone::clone(&self.id),
            name: ::core::clone::Clone::clone(&self.name),
            str_ref: ::core::clone::Clone::clone(&self.str_ref),
        }
    }
}Clone, #[automatically_derived]
impl ::core::default::Default for SetNamedType {
    #[inline]
    fn default() -> SetNamedType {
        SetNamedType {
            id: ::core::default::Default::default(),
            name: ::core::default::Default::default(),
            str_ref: ::core::default::Default::default(),
        }
    }
}Default, #[automatically_derived]
impl ::core::cmp::PartialEq for SetNamedType {
    #[inline]
    fn eq(&self, other: &SetNamedType) -> bool {
        self.id == other.id && self.name == other.name &&
            self.str_ref == other.str_ref
    }
}PartialEq, #[automatically_derived]
impl ::core::cmp::Eq for SetNamedType {
    #[inline]
    #[doc(hidden)]
    #[coverage(off)]
    fn assert_fields_are_eq(&self) {
        let _: ::core::cmp::AssertParamIsEq<u32>;
        let _: ::core::cmp::AssertParamIsEq<Option<String>>;
        let _: ::core::cmp::AssertParamIsEq<Option<i32>>;
    }
}Eq)]
245pub struct SetNamedType {
246    /// Entry id from the section suffix.
247    pub id:      u32,
248    /// Display or symbolic name.
249    pub name:    Option<String>,
250    /// Optional dialog.tlk string reference.
251    pub str_ref: Option<i32>,
252}
253
254/// One terrain corner annotation on a tile.
255///
256/// # Examples
257///
258/// ```rust,no_run
259/// let corner = nwnrs_set::SetTileCorner::default();
260/// assert!(corner.terrain.is_none());
261/// ```
262#[derive(#[automatically_derived]
impl ::core::fmt::Debug for SetTileCorner {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        ::core::fmt::Formatter::debug_struct_field2_finish(f, "SetTileCorner",
            "terrain", &self.terrain, "height", &&self.height)
    }
}Debug, #[automatically_derived]
impl ::core::clone::Clone for SetTileCorner {
    #[inline]
    fn clone(&self) -> SetTileCorner {
        SetTileCorner {
            terrain: ::core::clone::Clone::clone(&self.terrain),
            height: ::core::clone::Clone::clone(&self.height),
        }
    }
}Clone, #[automatically_derived]
impl ::core::default::Default for SetTileCorner {
    #[inline]
    fn default() -> SetTileCorner {
        SetTileCorner {
            terrain: ::core::default::Default::default(),
            height: ::core::default::Default::default(),
        }
    }
}Default, #[automatically_derived]
impl ::core::cmp::PartialEq for SetTileCorner {
    #[inline]
    fn eq(&self, other: &SetTileCorner) -> bool {
        self.terrain == other.terrain && self.height == other.height
    }
}PartialEq, #[automatically_derived]
impl ::core::cmp::Eq for SetTileCorner {
    #[inline]
    #[doc(hidden)]
    #[coverage(off)]
    fn assert_fields_are_eq(&self) {
        let _: ::core::cmp::AssertParamIsEq<Option<String>>;
        let _: ::core::cmp::AssertParamIsEq<Option<i32>>;
    }
}Eq)]
263pub struct SetTileCorner {
264    /// Terrain tag for this corner.
265    pub terrain: Option<String>,
266    /// Height step at this corner.
267    pub height:  Option<i32>,
268}
269
270/// One set of edge crosser tags on a tile.
271///
272/// # Examples
273///
274/// ```rust,no_run
275/// let edges = nwnrs_set::SetTileEdges::default();
276/// assert!(edges.top.is_none());
277/// ```
278#[derive(#[automatically_derived]
impl ::core::fmt::Debug for SetTileEdges {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        ::core::fmt::Formatter::debug_struct_field4_finish(f, "SetTileEdges",
            "top", &self.top, "right", &self.right, "bottom", &self.bottom,
            "left", &&self.left)
    }
}Debug, #[automatically_derived]
impl ::core::clone::Clone for SetTileEdges {
    #[inline]
    fn clone(&self) -> SetTileEdges {
        SetTileEdges {
            top: ::core::clone::Clone::clone(&self.top),
            right: ::core::clone::Clone::clone(&self.right),
            bottom: ::core::clone::Clone::clone(&self.bottom),
            left: ::core::clone::Clone::clone(&self.left),
        }
    }
}Clone, #[automatically_derived]
impl ::core::default::Default for SetTileEdges {
    #[inline]
    fn default() -> SetTileEdges {
        SetTileEdges {
            top: ::core::default::Default::default(),
            right: ::core::default::Default::default(),
            bottom: ::core::default::Default::default(),
            left: ::core::default::Default::default(),
        }
    }
}Default, #[automatically_derived]
impl ::core::cmp::PartialEq for SetTileEdges {
    #[inline]
    fn eq(&self, other: &SetTileEdges) -> bool {
        self.top == other.top && self.right == other.right &&
                self.bottom == other.bottom && self.left == other.left
    }
}PartialEq, #[automatically_derived]
impl ::core::cmp::Eq for SetTileEdges {
    #[inline]
    #[doc(hidden)]
    #[coverage(off)]
    fn assert_fields_are_eq(&self) {
        let _: ::core::cmp::AssertParamIsEq<Option<String>>;
        let _: ::core::cmp::AssertParamIsEq<Option<String>>;
        let _: ::core::cmp::AssertParamIsEq<Option<String>>;
        let _: ::core::cmp::AssertParamIsEq<Option<String>>;
    }
}Eq)]
279pub struct SetTileEdges {
280    /// Crosser tag on the top edge.
281    pub top:    Option<String>,
282    /// Crosser tag on the right edge.
283    pub right:  Option<String>,
284    /// Crosser tag on the bottom edge.
285    pub bottom: Option<String>,
286    /// Crosser tag on the left edge.
287    pub left:   Option<String>,
288}
289
290/// Parsed `[TILEN]` section.
291///
292/// # Examples
293///
294/// ```rust,no_run
295/// let tile = nwnrs_set::SetTile::default();
296/// assert_eq!(tile.id, 0);
297/// ```
298#[derive(#[automatically_derived]
impl ::core::fmt::Debug for SetTile {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        let names: &'static _ =
            &["id", "model", "walkmesh", "top_left", "top_right",
                        "bottom_left", "bottom_right", "edge_crossers",
                        "main_light_1", "main_light_2", "source_light_1",
                        "source_light_2", "anim_loop_1", "anim_loop_2",
                        "anim_loop_3", "doors", "sounds", "path_node",
                        "orientation", "visibility_node", "visibility_orientation",
                        "door_visibility_node", "door_visibility_orientation",
                        "image_map_2d"];
        let values: &[&dyn ::core::fmt::Debug] =
            &[&self.id, &self.model, &self.walkmesh, &self.top_left,
                        &self.top_right, &self.bottom_left, &self.bottom_right,
                        &self.edge_crossers, &self.main_light_1, &self.main_light_2,
                        &self.source_light_1, &self.source_light_2,
                        &self.anim_loop_1, &self.anim_loop_2, &self.anim_loop_3,
                        &self.doors, &self.sounds, &self.path_node,
                        &self.orientation, &self.visibility_node,
                        &self.visibility_orientation, &self.door_visibility_node,
                        &self.door_visibility_orientation, &&self.image_map_2d];
        ::core::fmt::Formatter::debug_struct_fields_finish(f, "SetTile",
            names, values)
    }
}Debug, #[automatically_derived]
impl ::core::clone::Clone for SetTile {
    #[inline]
    fn clone(&self) -> SetTile {
        SetTile {
            id: ::core::clone::Clone::clone(&self.id),
            model: ::core::clone::Clone::clone(&self.model),
            walkmesh: ::core::clone::Clone::clone(&self.walkmesh),
            top_left: ::core::clone::Clone::clone(&self.top_left),
            top_right: ::core::clone::Clone::clone(&self.top_right),
            bottom_left: ::core::clone::Clone::clone(&self.bottom_left),
            bottom_right: ::core::clone::Clone::clone(&self.bottom_right),
            edge_crossers: ::core::clone::Clone::clone(&self.edge_crossers),
            main_light_1: ::core::clone::Clone::clone(&self.main_light_1),
            main_light_2: ::core::clone::Clone::clone(&self.main_light_2),
            source_light_1: ::core::clone::Clone::clone(&self.source_light_1),
            source_light_2: ::core::clone::Clone::clone(&self.source_light_2),
            anim_loop_1: ::core::clone::Clone::clone(&self.anim_loop_1),
            anim_loop_2: ::core::clone::Clone::clone(&self.anim_loop_2),
            anim_loop_3: ::core::clone::Clone::clone(&self.anim_loop_3),
            doors: ::core::clone::Clone::clone(&self.doors),
            sounds: ::core::clone::Clone::clone(&self.sounds),
            path_node: ::core::clone::Clone::clone(&self.path_node),
            orientation: ::core::clone::Clone::clone(&self.orientation),
            visibility_node: ::core::clone::Clone::clone(&self.visibility_node),
            visibility_orientation: ::core::clone::Clone::clone(&self.visibility_orientation),
            door_visibility_node: ::core::clone::Clone::clone(&self.door_visibility_node),
            door_visibility_orientation: ::core::clone::Clone::clone(&self.door_visibility_orientation),
            image_map_2d: ::core::clone::Clone::clone(&self.image_map_2d),
        }
    }
}Clone, #[automatically_derived]
impl ::core::default::Default for SetTile {
    #[inline]
    fn default() -> SetTile {
        SetTile {
            id: ::core::default::Default::default(),
            model: ::core::default::Default::default(),
            walkmesh: ::core::default::Default::default(),
            top_left: ::core::default::Default::default(),
            top_right: ::core::default::Default::default(),
            bottom_left: ::core::default::Default::default(),
            bottom_right: ::core::default::Default::default(),
            edge_crossers: ::core::default::Default::default(),
            main_light_1: ::core::default::Default::default(),
            main_light_2: ::core::default::Default::default(),
            source_light_1: ::core::default::Default::default(),
            source_light_2: ::core::default::Default::default(),
            anim_loop_1: ::core::default::Default::default(),
            anim_loop_2: ::core::default::Default::default(),
            anim_loop_3: ::core::default::Default::default(),
            doors: ::core::default::Default::default(),
            sounds: ::core::default::Default::default(),
            path_node: ::core::default::Default::default(),
            orientation: ::core::default::Default::default(),
            visibility_node: ::core::default::Default::default(),
            visibility_orientation: ::core::default::Default::default(),
            door_visibility_node: ::core::default::Default::default(),
            door_visibility_orientation: ::core::default::Default::default(),
            image_map_2d: ::core::default::Default::default(),
        }
    }
}Default, #[automatically_derived]
impl ::core::cmp::PartialEq for SetTile {
    #[inline]
    fn eq(&self, other: &SetTile) -> bool {
        self.id == other.id && self.model == other.model &&
                                                                                                self.walkmesh == other.walkmesh &&
                                                                                            self.top_left == other.top_left &&
                                                                                        self.top_right == other.top_right &&
                                                                                    self.bottom_left == other.bottom_left &&
                                                                                self.bottom_right == other.bottom_right &&
                                                                            self.edge_crossers == other.edge_crossers &&
                                                                        self.main_light_1 == other.main_light_1 &&
                                                                    self.main_light_2 == other.main_light_2 &&
                                                                self.source_light_1 == other.source_light_1 &&
                                                            self.source_light_2 == other.source_light_2 &&
                                                        self.anim_loop_1 == other.anim_loop_1 &&
                                                    self.anim_loop_2 == other.anim_loop_2 &&
                                                self.anim_loop_3 == other.anim_loop_3 &&
                                            self.doors == other.doors && self.sounds == other.sounds &&
                                    self.path_node == other.path_node &&
                                self.orientation == other.orientation &&
                            self.visibility_node == other.visibility_node &&
                        self.visibility_orientation == other.visibility_orientation
                    && self.door_visibility_node == other.door_visibility_node
                &&
                self.door_visibility_orientation ==
                    other.door_visibility_orientation &&
            self.image_map_2d == other.image_map_2d
    }
}PartialEq, #[automatically_derived]
impl ::core::cmp::Eq for SetTile {
    #[inline]
    #[doc(hidden)]
    #[coverage(off)]
    fn assert_fields_are_eq(&self) {
        let _: ::core::cmp::AssertParamIsEq<u32>;
        let _: ::core::cmp::AssertParamIsEq<Option<String>>;
        let _: ::core::cmp::AssertParamIsEq<Option<String>>;
        let _: ::core::cmp::AssertParamIsEq<SetTileCorner>;
        let _: ::core::cmp::AssertParamIsEq<SetTileEdges>;
        let _: ::core::cmp::AssertParamIsEq<Option<bool>>;
        let _: ::core::cmp::AssertParamIsEq<Option<bool>>;
        let _: ::core::cmp::AssertParamIsEq<Option<bool>>;
        let _: ::core::cmp::AssertParamIsEq<Option<bool>>;
        let _: ::core::cmp::AssertParamIsEq<Option<bool>>;
        let _: ::core::cmp::AssertParamIsEq<Option<bool>>;
        let _: ::core::cmp::AssertParamIsEq<Option<bool>>;
        let _: ::core::cmp::AssertParamIsEq<Option<u32>>;
        let _: ::core::cmp::AssertParamIsEq<Option<u32>>;
        let _: ::core::cmp::AssertParamIsEq<Option<String>>;
        let _: ::core::cmp::AssertParamIsEq<Option<i32>>;
        let _: ::core::cmp::AssertParamIsEq<Option<String>>;
        let _: ::core::cmp::AssertParamIsEq<Option<i32>>;
        let _: ::core::cmp::AssertParamIsEq<Option<String>>;
        let _: ::core::cmp::AssertParamIsEq<Option<i32>>;
        let _: ::core::cmp::AssertParamIsEq<Option<String>>;
    }
}Eq)]
299pub struct SetTile {
300    /// Tile id from the section suffix.
301    pub id: u32,
302    /// MDL resource name.
303    pub model: Option<String>,
304    /// Walkmesh identifier.
305    pub walkmesh: Option<String>,
306    /// Top-left terrain annotation.
307    pub top_left: SetTileCorner,
308    /// Top-right terrain annotation.
309    pub top_right: SetTileCorner,
310    /// Bottom-left terrain annotation.
311    pub bottom_left: SetTileCorner,
312    /// Bottom-right terrain annotation.
313    pub bottom_right: SetTileCorner,
314    /// Edge crosser tags.
315    pub edge_crossers: SetTileEdges,
316    /// First main-light flag.
317    pub main_light_1: Option<bool>,
318    /// Second main-light flag.
319    pub main_light_2: Option<bool>,
320    /// First source-light flag.
321    pub source_light_1: Option<bool>,
322    /// Second source-light flag.
323    pub source_light_2: Option<bool>,
324    /// First animation-loop flag.
325    pub anim_loop_1: Option<bool>,
326    /// Second animation-loop flag.
327    pub anim_loop_2: Option<bool>,
328    /// Third animation-loop flag.
329    pub anim_loop_3: Option<bool>,
330    /// Door count declared on the tile.
331    pub doors: Option<u32>,
332    /// Sound count declared on the tile.
333    pub sounds: Option<u32>,
334    /// Path node marker.
335    pub path_node: Option<String>,
336    /// Path node orientation.
337    pub orientation: Option<i32>,
338    /// Visibility node marker.
339    pub visibility_node: Option<String>,
340    /// Visibility node orientation.
341    pub visibility_orientation: Option<i32>,
342    /// Optional door visibility node marker.
343    pub door_visibility_node: Option<String>,
344    /// Optional door visibility node orientation.
345    pub door_visibility_orientation: Option<i32>,
346    /// 2D selector image name.
347    pub image_map_2d: Option<String>,
348}
349
350/// Parsed `[TILENDOORK]` section.
351///
352/// # Examples
353///
354/// ```rust,no_run
355/// let door = nwnrs_set::SetTileDoor::default();
356/// assert_eq!(door.tile_id, 0);
357/// ```
358#[derive(#[automatically_derived]
impl ::core::fmt::Debug for SetTileDoor {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        let names: &'static _ =
            &["tile_id", "door_id", "door_type", "x", "y", "z",
                        "orientation"];
        let values: &[&dyn ::core::fmt::Debug] =
            &[&self.tile_id, &self.door_id, &self.door_type, &self.x, &self.y,
                        &self.z, &&self.orientation];
        ::core::fmt::Formatter::debug_struct_fields_finish(f, "SetTileDoor",
            names, values)
    }
}Debug, #[automatically_derived]
impl ::core::clone::Clone for SetTileDoor {
    #[inline]
    fn clone(&self) -> SetTileDoor {
        SetTileDoor {
            tile_id: ::core::clone::Clone::clone(&self.tile_id),
            door_id: ::core::clone::Clone::clone(&self.door_id),
            door_type: ::core::clone::Clone::clone(&self.door_type),
            x: ::core::clone::Clone::clone(&self.x),
            y: ::core::clone::Clone::clone(&self.y),
            z: ::core::clone::Clone::clone(&self.z),
            orientation: ::core::clone::Clone::clone(&self.orientation),
        }
    }
}Clone, #[automatically_derived]
impl ::core::default::Default for SetTileDoor {
    #[inline]
    fn default() -> SetTileDoor {
        SetTileDoor {
            tile_id: ::core::default::Default::default(),
            door_id: ::core::default::Default::default(),
            door_type: ::core::default::Default::default(),
            x: ::core::default::Default::default(),
            y: ::core::default::Default::default(),
            z: ::core::default::Default::default(),
            orientation: ::core::default::Default::default(),
        }
    }
}Default, #[automatically_derived]
impl ::core::cmp::PartialEq for SetTileDoor {
    #[inline]
    fn eq(&self, other: &SetTileDoor) -> bool {
        self.tile_id == other.tile_id && self.door_id == other.door_id &&
                            self.door_type == other.door_type && self.x == other.x &&
                    self.y == other.y && self.z == other.z &&
            self.orientation == other.orientation
    }
}PartialEq)]
359pub struct SetTileDoor {
360    /// Tile id from the section prefix.
361    pub tile_id:     u32,
362    /// Door id from the section suffix.
363    pub door_id:     u32,
364    /// Door type identifier.
365    pub door_type:   Option<i32>,
366    /// Door marker X coordinate.
367    pub x:           Option<f32>,
368    /// Door marker Y coordinate.
369    pub y:           Option<f32>,
370    /// Door marker Z coordinate.
371    pub z:           Option<f32>,
372    /// Door marker orientation.
373    pub orientation: Option<i32>,
374}
375
376/// Parsed `[GROUPN]` section.
377///
378/// # Examples
379///
380/// ```rust,no_run
381/// let group = nwnrs_set::SetGroup::default();
382/// assert!(group.tiles.is_empty());
383/// ```
384#[derive(#[automatically_derived]
impl ::core::fmt::Debug for SetGroup {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        let names: &'static _ =
            &["id", "name", "str_ref", "rows", "columns", "tiles"];
        let values: &[&dyn ::core::fmt::Debug] =
            &[&self.id, &self.name, &self.str_ref, &self.rows, &self.columns,
                        &&self.tiles];
        ::core::fmt::Formatter::debug_struct_fields_finish(f, "SetGroup",
            names, values)
    }
}Debug, #[automatically_derived]
impl ::core::clone::Clone for SetGroup {
    #[inline]
    fn clone(&self) -> SetGroup {
        SetGroup {
            id: ::core::clone::Clone::clone(&self.id),
            name: ::core::clone::Clone::clone(&self.name),
            str_ref: ::core::clone::Clone::clone(&self.str_ref),
            rows: ::core::clone::Clone::clone(&self.rows),
            columns: ::core::clone::Clone::clone(&self.columns),
            tiles: ::core::clone::Clone::clone(&self.tiles),
        }
    }
}Clone, #[automatically_derived]
impl ::core::default::Default for SetGroup {
    #[inline]
    fn default() -> SetGroup {
        SetGroup {
            id: ::core::default::Default::default(),
            name: ::core::default::Default::default(),
            str_ref: ::core::default::Default::default(),
            rows: ::core::default::Default::default(),
            columns: ::core::default::Default::default(),
            tiles: ::core::default::Default::default(),
        }
    }
}Default, #[automatically_derived]
impl ::core::cmp::PartialEq for SetGroup {
    #[inline]
    fn eq(&self, other: &SetGroup) -> bool {
        self.id == other.id && self.name == other.name &&
                        self.str_ref == other.str_ref && self.rows == other.rows &&
                self.columns == other.columns && self.tiles == other.tiles
    }
}PartialEq, #[automatically_derived]
impl ::core::cmp::Eq for SetGroup {
    #[inline]
    #[doc(hidden)]
    #[coverage(off)]
    fn assert_fields_are_eq(&self) {
        let _: ::core::cmp::AssertParamIsEq<u32>;
        let _: ::core::cmp::AssertParamIsEq<Option<String>>;
        let _: ::core::cmp::AssertParamIsEq<Option<i32>>;
        let _: ::core::cmp::AssertParamIsEq<Option<u32>>;
        let _: ::core::cmp::AssertParamIsEq<Option<u32>>;
        let _: ::core::cmp::AssertParamIsEq<BTreeMap<u32, Option<u32>>>;
    }
}Eq)]
385pub struct SetGroup {
386    /// Group id from the section suffix.
387    pub id:      u32,
388    /// Group display name.
389    pub name:    Option<String>,
390    /// Optional dialog.tlk string reference.
391    pub str_ref: Option<i32>,
392    /// Group row count.
393    pub rows:    Option<u32>,
394    /// Group column count.
395    pub columns: Option<u32>,
396    /// Group tile layout keyed by zero-based cell index.
397    pub tiles:   BTreeMap<u32, Option<u32>>,
398}
399
400/// Parsed `[PRIMARY RULEN]` section.
401///
402/// # Examples
403///
404/// ```rust,no_run
405/// let rule = nwnrs_set::SetPrimaryRule::default();
406/// assert_eq!(rule.id, 0);
407/// ```
408#[derive(#[automatically_derived]
impl ::core::fmt::Debug for SetPrimaryRule {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        let names: &'static _ =
            &["id", "placed", "placed_height", "adjacent", "adjacent_height",
                        "changed", "changed_height"];
        let values: &[&dyn ::core::fmt::Debug] =
            &[&self.id, &self.placed, &self.placed_height, &self.adjacent,
                        &self.adjacent_height, &self.changed,
                        &&self.changed_height];
        ::core::fmt::Formatter::debug_struct_fields_finish(f,
            "SetPrimaryRule", names, values)
    }
}Debug, #[automatically_derived]
impl ::core::clone::Clone for SetPrimaryRule {
    #[inline]
    fn clone(&self) -> SetPrimaryRule {
        SetPrimaryRule {
            id: ::core::clone::Clone::clone(&self.id),
            placed: ::core::clone::Clone::clone(&self.placed),
            placed_height: ::core::clone::Clone::clone(&self.placed_height),
            adjacent: ::core::clone::Clone::clone(&self.adjacent),
            adjacent_height: ::core::clone::Clone::clone(&self.adjacent_height),
            changed: ::core::clone::Clone::clone(&self.changed),
            changed_height: ::core::clone::Clone::clone(&self.changed_height),
        }
    }
}Clone, #[automatically_derived]
impl ::core::default::Default for SetPrimaryRule {
    #[inline]
    fn default() -> SetPrimaryRule {
        SetPrimaryRule {
            id: ::core::default::Default::default(),
            placed: ::core::default::Default::default(),
            placed_height: ::core::default::Default::default(),
            adjacent: ::core::default::Default::default(),
            adjacent_height: ::core::default::Default::default(),
            changed: ::core::default::Default::default(),
            changed_height: ::core::default::Default::default(),
        }
    }
}Default, #[automatically_derived]
impl ::core::cmp::PartialEq for SetPrimaryRule {
    #[inline]
    fn eq(&self, other: &SetPrimaryRule) -> bool {
        self.id == other.id && self.placed == other.placed &&
                            self.placed_height == other.placed_height &&
                        self.adjacent == other.adjacent &&
                    self.adjacent_height == other.adjacent_height &&
                self.changed == other.changed &&
            self.changed_height == other.changed_height
    }
}PartialEq, #[automatically_derived]
impl ::core::cmp::Eq for SetPrimaryRule {
    #[inline]
    #[doc(hidden)]
    #[coverage(off)]
    fn assert_fields_are_eq(&self) {
        let _: ::core::cmp::AssertParamIsEq<u32>;
        let _: ::core::cmp::AssertParamIsEq<Option<String>>;
        let _: ::core::cmp::AssertParamIsEq<Option<i32>>;
        let _: ::core::cmp::AssertParamIsEq<Option<String>>;
        let _: ::core::cmp::AssertParamIsEq<Option<i32>>;
        let _: ::core::cmp::AssertParamIsEq<Option<String>>;
        let _: ::core::cmp::AssertParamIsEq<Option<i32>>;
    }
}Eq)]
409pub struct SetPrimaryRule {
410    /// Rule id from the section suffix.
411    pub id:              u32,
412    /// Terrain tag for the placed tile.
413    pub placed:          Option<String>,
414    /// Height for the placed terrain.
415    pub placed_height:   Option<i32>,
416    /// Terrain tag for the adjacent tile.
417    pub adjacent:        Option<String>,
418    /// Height for the adjacent terrain.
419    pub adjacent_height: Option<i32>,
420    /// Terrain tag after applying the rule.
421    pub changed:         Option<String>,
422    /// Height after applying the rule.
423    pub changed_height:  Option<i32>,
424}
425
426/// Reads a typed `SET` file from `reader`.
427///
428/// # Errors
429///
430/// Returns [`SetError`] if the data cannot be read or does not conform to the
431/// SET format.
432///
433/// # Examples
434///
435/// ```rust,no_run
436/// let _ = nwnrs_set::read_set;
437/// ```
438#[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: SetResult<SetFile> =
                            loop {};
                        return __tracing_attr_fake_return;
                    }
                    {
                        let mut text = String::new();
                        reader.read_to_string(&mut text)?;
                        parse_set(&text)
                    }
                })()
    {
        #[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:438",
                                "nwnrs_set", ::tracing::Level::ERROR,
                                ::tracing_core::__macro_support::Option::Some("src/lib.rs"),
                                ::tracing_core::__macro_support::Option::Some(438u32),
                                ::tracing_core::__macro_support::Option::Some("nwnrs_set"),
                                ::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)]
439pub fn read_set<R: Read>(reader: &mut R) -> SetResult<SetFile> {
440    let mut text = String::new();
441    reader.read_to_string(&mut text)?;
442    parse_set(&text)
443}
444
445/// Parses a typed `SET` file from text.
446///
447/// # Errors
448///
449/// Returns [`SetError`] if the text contains no tile definitions.
450///
451/// # Examples
452///
453/// ```rust,no_run
454/// let _ = nwnrs_set::parse_set;
455/// ```
456pub fn parse_set(text: &str) -> SetResult<SetFile> {
457    let mut builder = SetFile::default();
458    let mut current_section = String::new();
459    let mut current_entries = BTreeMap::new();
460
461    for raw_line in text.lines() {
462        let line = raw_line.trim();
463        if line.is_empty() || line.starts_with(';') || line.starts_with("//") {
464            continue;
465        }
466
467        if line.starts_with('[') && line.ends_with(']') {
468            if !current_section.is_empty() {
469                apply_section(&mut builder, &current_section, &current_entries);
470                current_entries.clear();
471            }
472            current_section = line[1..line.len() - 1].trim().to_string();
473            continue;
474        }
475
476        let Some((key, value)) = line.split_once('=') else {
477            continue;
478        };
479        current_entries.insert(key.trim().to_ascii_lowercase(), value.trim().to_string());
480    }
481
482    if !current_section.is_empty() {
483        apply_section(&mut builder, &current_section, &current_entries);
484    }
485
486    if builder.tiles.is_empty() {
487        return Err(SetError::msg(
488            "tileset file contained no tile definitions".to_string(),
489        ));
490    }
491
492    Ok(builder)
493}
494
495/// Builds deterministic `SET` text from a typed [`SetFile`].
496///
497/// The serializer emits the modeled section structure explicitly: general and
498/// grass blocks first, followed by synthesized catalog count sections and then
499/// the indexed terrain, crosser, rule, tile, tile-door, and group sections in
500/// ascending key order.
501///
502/// # Errors
503///
504/// Returns [`SetError`] if the file contains no tile definitions.
505///
506/// # Examples
507///
508/// ```rust,no_run
509/// let _ = nwnrs_set::build_set_text;
510/// ```
511pub fn build_set_text(set_file: &SetFile) -> SetResult<String> {
512    if set_file.tiles.is_empty() {
513        return Err(SetError::msg(
514            "cannot build SET payload without at least one tile definition",
515        ));
516    }
517
518    let mut text = String::new();
519    write_general(&mut text, &set_file.general);
520    if let Some(grass) = &set_file.grass {
521        push_blank_line(&mut text);
522        write_grass(&mut text, grass);
523    }
524
525    push_blank_line(&mut text);
526    write_count_section(&mut text, "TERRAIN TYPES", set_file.terrains.len());
527    for (id, terrain) in &set_file.terrains {
528        push_blank_line(&mut text);
529        write_named_type_section(&mut text, &::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("TERRAIN{0}", id))
    })format!("TERRAIN{id}"), terrain);
530    }
531
532    push_blank_line(&mut text);
533    write_count_section(&mut text, "CROSSER TYPES", set_file.crossers.len());
534    for (id, crosser) in &set_file.crossers {
535        push_blank_line(&mut text);
536        write_named_type_section(&mut text, &::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("CROSSER{0}", id))
    })format!("CROSSER{id}"), crosser);
537    }
538
539    push_blank_line(&mut text);
540    write_count_section(&mut text, "PRIMARY RULES", set_file.primary_rules.len());
541    for (id, rule) in &set_file.primary_rules {
542        push_blank_line(&mut text);
543        write_primary_rule_section(&mut text, &::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("PRIMARY RULE{0}", id))
    })format!("PRIMARY RULE{id}"), rule);
544    }
545
546    push_blank_line(&mut text);
547    write_count_section(&mut text, "TILES", set_file.tiles.len());
548    for (id, tile) in &set_file.tiles {
549        push_blank_line(&mut text);
550        write_tile_section(&mut text, &::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("TILE{0}", id))
    })format!("TILE{id}"), tile);
551    }
552
553    for ((tile_id, door_id), door) in &set_file.tile_doors {
554        push_blank_line(&mut text);
555        write_tile_door_section(&mut text, &::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("TILE{0}DOOR{1}", tile_id, door_id))
    })format!("TILE{tile_id}DOOR{door_id}"), door);
556    }
557
558    push_blank_line(&mut text);
559    write_count_section(&mut text, "GROUPS", set_file.groups.len());
560    for (id, group) in &set_file.groups {
561        push_blank_line(&mut text);
562        write_group_section(&mut text, &::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("GROUP{0}", id))
    })format!("GROUP{id}"), group);
563    }
564
565    Ok(text)
566}
567
568/// Writes deterministic `SET` text to `writer`.
569///
570/// # Errors
571///
572/// Returns [`SetError`] if the file contains no tile definitions or the write
573/// fails.
574///
575/// # Examples
576///
577/// ```rust,no_run
578/// let _ = nwnrs_set::write_set;
579/// ```
580pub fn write_set<W: Write>(writer: &mut W, set_file: &SetFile) -> SetResult<()> {
581    let text = build_set_text(set_file)?;
582    writer.write_all(text.as_bytes())?;
583    Ok(())
584}
585
586fn apply_section(set_file: &mut SetFile, section_name: &str, entries: &BTreeMap<String, String>) {
587    let section_upper = section_name.to_ascii_uppercase();
588
589    match section_upper.as_str() {
590        "GENERAL" => set_file.general = parse_general(entries),
591        "GRASS" => set_file.grass = Some(parse_grass(entries)),
592        "TERRAIN TYPES" | "CROSSER TYPES" | "PRIMARY RULES" | "SECONDARY RULES" | "TILES"
593        | "GROUPS" => {}
594        _ => {
595            if let Some(index) = parse_indexed_section(&section_upper, "TERRAIN") {
596                set_file
597                    .terrains
598                    .insert(index, parse_named_type(index, entries));
599            } else if let Some(index) = parse_indexed_section(&section_upper, "CROSSER") {
600                set_file
601                    .crossers
602                    .insert(index, parse_named_type(index, entries));
603            } else if let Some(index) = parse_indexed_section(&section_upper, "GROUP") {
604                set_file.groups.insert(index, parse_group(index, entries));
605            } else if let Some(index) = parse_indexed_section(&section_upper, "PRIMARY RULE") {
606                set_file
607                    .primary_rules
608                    .insert(index, parse_primary_rule(index, entries));
609            } else if let Some((tile_id, door_id)) = parse_tile_door_section(&section_upper) {
610                set_file.tile_doors.insert(
611                    (tile_id, door_id),
612                    parse_tile_door(tile_id, door_id, entries),
613                );
614            } else if let Some(index) = parse_indexed_section(&section_upper, "TILE") {
615                set_file.tiles.insert(index, parse_tile(index, entries));
616            }
617        }
618    }
619}
620
621fn parse_general(entries: &BTreeMap<String, String>) -> SetGeneral {
622    SetGeneral {
623        name:                  read_text(entries, "name"),
624        file_type:             read_text(entries, "type"),
625        version:               read_text(entries, "version"),
626        interior:              read_bool(entries, "interior"),
627        has_height_transition: read_bool(entries, "hasheighttransition"),
628        env_map:               read_text(entries, "envmap"),
629        transition:            read_i32(entries, "transition"),
630        selector_height:       read_i32(entries, "selectorheight"),
631        display_name:          read_i32(entries, "displayname"),
632        unlocalized_name:      read_text(entries, "unlocalizedname"),
633        border:                read_text(entries, "border"),
634        default_terrain:       read_text(entries, "default"),
635        floor:                 read_text(entries, "floor"),
636    }
637}
638
639fn parse_grass(entries: &BTreeMap<String, String>) -> SetGrass {
640    SetGrass {
641        grass:        read_bool(entries, "grass"),
642        texture_name: read_text(entries, "grasstexturename"),
643        density:      read_f32(entries, "density"),
644        height:       read_f32(entries, "height"),
645        ambient:      parse_rgb(entries, "ambientred", "ambientgreen", "ambientblue"),
646        diffuse:      parse_rgb(entries, "diffusered", "diffusegreen", "diffuseblue"),
647    }
648}
649
650fn parse_named_type(id: u32, entries: &BTreeMap<String, String>) -> SetNamedType {
651    SetNamedType {
652        id,
653        name: read_text(entries, "name"),
654        str_ref: read_i32(entries, "strref"),
655    }
656}
657
658fn parse_group(id: u32, entries: &BTreeMap<String, String>) -> SetGroup {
659    let mut tiles = BTreeMap::new();
660    for (key, value) in entries {
661        if let Some(index) = key
662            .strip_prefix("tile")
663            .and_then(|suffix| suffix.parse::<u32>().ok())
664        {
665            tiles.insert(
666                index,
667                value
668                    .parse::<i32>()
669                    .ok()
670                    .and_then(|raw| u32::try_from(raw).ok()),
671            );
672        }
673    }
674
675    SetGroup {
676        id,
677        name: read_text(entries, "name"),
678        str_ref: read_i32(entries, "strref"),
679        rows: read_u32(entries, "rows"),
680        columns: read_u32(entries, "columns"),
681        tiles,
682    }
683}
684
685fn parse_primary_rule(id: u32, entries: &BTreeMap<String, String>) -> SetPrimaryRule {
686    SetPrimaryRule {
687        id,
688        placed: read_text(entries, "placed"),
689        placed_height: read_i32(entries, "placedheight"),
690        adjacent: read_text(entries, "adjacent"),
691        adjacent_height: read_i32(entries, "adjacentheight"),
692        changed: read_text(entries, "changed"),
693        changed_height: read_i32(entries, "changedheight"),
694    }
695}
696
697fn parse_tile(id: u32, entries: &BTreeMap<String, String>) -> SetTile {
698    SetTile {
699        id,
700        model: read_text(entries, "model"),
701        walkmesh: read_text(entries, "walkmesh"),
702        top_left: parse_tile_corner(entries, "topleft", "topleftheight"),
703        top_right: parse_tile_corner(entries, "topright", "toprightheight"),
704        bottom_left: parse_tile_corner(entries, "bottomleft", "bottomleftheight"),
705        bottom_right: parse_tile_corner(entries, "bottomright", "bottomrightheight"),
706        edge_crossers: SetTileEdges {
707            top:    read_text(entries, "top"),
708            right:  read_text(entries, "right"),
709            bottom: read_text(entries, "bottom"),
710            left:   read_text(entries, "left"),
711        },
712        main_light_1: read_bool(entries, "mainlight1"),
713        main_light_2: read_bool(entries, "mainlight2"),
714        source_light_1: read_bool(entries, "sourcelight1"),
715        source_light_2: read_bool(entries, "sourcelight2"),
716        anim_loop_1: read_bool(entries, "animloop1"),
717        anim_loop_2: read_bool(entries, "animloop2"),
718        anim_loop_3: read_bool(entries, "animloop3"),
719        doors: read_u32(entries, "doors"),
720        sounds: read_u32(entries, "sounds"),
721        path_node: read_text(entries, "pathnode"),
722        orientation: read_i32(entries, "orientation"),
723        visibility_node: read_text(entries, "visibilitynode"),
724        visibility_orientation: read_i32(entries, "visibilityorientation"),
725        door_visibility_node: read_text(entries, "doorvisibilitynode"),
726        door_visibility_orientation: read_i32(entries, "doorvisibilityorientation"),
727        image_map_2d: read_text(entries, "imagemap2d"),
728    }
729}
730
731fn parse_tile_door(tile_id: u32, door_id: u32, entries: &BTreeMap<String, String>) -> SetTileDoor {
732    SetTileDoor {
733        tile_id,
734        door_id,
735        door_type: read_i32(entries, "type"),
736        x: read_f32(entries, "x"),
737        y: read_f32(entries, "y"),
738        z: read_f32(entries, "z"),
739        orientation: read_i32(entries, "orientation"),
740    }
741}
742
743fn parse_tile_corner(
744    entries: &BTreeMap<String, String>,
745    terrain_key: &str,
746    height_key: &str,
747) -> SetTileCorner {
748    SetTileCorner {
749        terrain: read_text(entries, terrain_key)
750            .filter(|value| !value.eq_ignore_ascii_case("invalid")),
751        height:  read_i32(entries, height_key),
752    }
753}
754
755fn parse_rgb(
756    entries: &BTreeMap<String, String>,
757    red_key: &str,
758    green_key: &str,
759    blue_key: &str,
760) -> Option<[f32; 3]> {
761    Some([
762        read_f32(entries, red_key)?,
763        read_f32(entries, green_key)?,
764        read_f32(entries, blue_key)?,
765    ])
766}
767
768fn parse_indexed_section(section_name: &str, prefix: &str) -> Option<u32> {
769    let suffix = section_name.strip_prefix(prefix)?;
770    if suffix.is_empty() {
771        return None;
772    }
773    suffix.parse::<u32>().ok()
774}
775
776fn parse_tile_door_section(section_name: &str) -> Option<(u32, u32)> {
777    let (tile_part, door_part) = section_name.split_once("DOOR")?;
778    let tile_id = tile_part.strip_prefix("TILE")?.parse::<u32>().ok()?;
779    let door_id = door_part.parse::<u32>().ok()?;
780    Some((tile_id, door_id))
781}
782
783fn read_text(entries: &BTreeMap<String, String>, key: &str) -> Option<String> {
784    let value = entries.get(key)?.trim().trim_matches('"');
785    if value.is_empty() || value == "****" {
786        return None;
787    }
788    Some(value.to_string())
789}
790
791fn read_bool(entries: &BTreeMap<String, String>, key: &str) -> Option<bool> {
792    let value = entries.get(key)?.trim();
793    match value {
794        "1" => Some(true),
795        "0" => Some(false),
796        _ if value.eq_ignore_ascii_case("true") => Some(true),
797        _ if value.eq_ignore_ascii_case("false") => Some(false),
798        _ => None,
799    }
800}
801
802fn read_u32(entries: &BTreeMap<String, String>, key: &str) -> Option<u32> {
803    entries.get(key)?.trim().parse::<u32>().ok()
804}
805
806fn read_i32(entries: &BTreeMap<String, String>, key: &str) -> Option<i32> {
807    entries.get(key)?.trim().parse::<i32>().ok()
808}
809
810fn read_f32(entries: &BTreeMap<String, String>, key: &str) -> Option<f32> {
811    entries.get(key)?.trim().parse::<f32>().ok()
812}
813
814fn push_blank_line(text: &mut String) {
815    if !text.is_empty() && !text.ends_with("\n\n") {
816        text.push('\n');
817    }
818}
819
820fn write_section_header(text: &mut String, name: &str) {
821    text.push('[');
822    text.push_str(name);
823    text.push_str("]\n");
824}
825
826fn write_string_value(text: &mut String, key: &str, value: Option<&String>) {
827    if let Some(value) = value {
828        text.push_str(key);
829        text.push('=');
830        text.push_str(value);
831        text.push('\n');
832    }
833}
834
835fn write_bool_value(text: &mut String, key: &str, value: Option<bool>) {
836    if let Some(value) = value {
837        text.push_str(key);
838        text.push('=');
839        text.push_str(if value { "1" } else { "0" });
840        text.push('\n');
841    }
842}
843
844fn write_u32_value(text: &mut String, key: &str, value: Option<u32>) {
845    if let Some(value) = value {
846        text.push_str(key);
847        text.push('=');
848        text.push_str(&value.to_string());
849        text.push('\n');
850    }
851}
852
853fn write_i32_value(text: &mut String, key: &str, value: Option<i32>) {
854    if let Some(value) = value {
855        text.push_str(key);
856        text.push('=');
857        text.push_str(&value.to_string());
858        text.push('\n');
859    }
860}
861
862fn write_f32_value(text: &mut String, key: &str, value: Option<f32>) {
863    if let Some(value) = value {
864        text.push_str(key);
865        text.push('=');
866        text.push_str(&value.to_string());
867        text.push('\n');
868    }
869}
870
871fn write_count_section(text: &mut String, name: &str, count: usize) {
872    write_section_header(text, name);
873    text.push_str("Count=");
874    text.push_str(&count.to_string());
875    text.push('\n');
876}
877
878fn write_general(text: &mut String, general: &SetGeneral) {
879    write_section_header(text, "GENERAL");
880    write_string_value(text, "Name", general.name.as_ref());
881    write_string_value(text, "Type", general.file_type.as_ref());
882    write_string_value(text, "Version", general.version.as_ref());
883    write_bool_value(text, "Interior", general.interior);
884    write_bool_value(text, "HasHeightTransition", general.has_height_transition);
885    write_string_value(text, "EnvMap", general.env_map.as_ref());
886    write_i32_value(text, "Transition", general.transition);
887    write_i32_value(text, "SelectorHeight", general.selector_height);
888    write_i32_value(text, "DisplayName", general.display_name);
889    write_string_value(text, "UnlocalizedName", general.unlocalized_name.as_ref());
890    write_string_value(text, "Border", general.border.as_ref());
891    write_string_value(text, "Default", general.default_terrain.as_ref());
892    write_string_value(text, "Floor", general.floor.as_ref());
893}
894
895fn write_grass(text: &mut String, grass: &SetGrass) {
896    write_section_header(text, "GRASS");
897    write_bool_value(text, "Grass", grass.grass);
898    write_string_value(text, "GrassTextureName", grass.texture_name.as_ref());
899    write_f32_value(text, "Density", grass.density);
900    write_f32_value(text, "Height", grass.height);
901    if let Some([red, green, blue]) = grass.ambient {
902        write_f32_value(text, "AmbientRed", Some(red));
903        write_f32_value(text, "AmbientGreen", Some(green));
904        write_f32_value(text, "AmbientBlue", Some(blue));
905    }
906    if let Some([red, green, blue]) = grass.diffuse {
907        write_f32_value(text, "DiffuseRed", Some(red));
908        write_f32_value(text, "DiffuseGreen", Some(green));
909        write_f32_value(text, "DiffuseBlue", Some(blue));
910    }
911}
912
913fn write_named_type_section(text: &mut String, section_name: &str, named_type: &SetNamedType) {
914    write_section_header(text, section_name);
915    write_string_value(text, "Name", named_type.name.as_ref());
916    write_i32_value(text, "StrRef", named_type.str_ref);
917}
918
919fn write_primary_rule_section(
920    text: &mut String,
921    section_name: &str,
922    primary_rule: &SetPrimaryRule,
923) {
924    write_section_header(text, section_name);
925    write_string_value(text, "Placed", primary_rule.placed.as_ref());
926    write_i32_value(text, "PlacedHeight", primary_rule.placed_height);
927    write_string_value(text, "Adjacent", primary_rule.adjacent.as_ref());
928    write_i32_value(text, "AdjacentHeight", primary_rule.adjacent_height);
929    write_string_value(text, "Changed", primary_rule.changed.as_ref());
930    write_i32_value(text, "ChangedHeight", primary_rule.changed_height);
931}
932
933fn write_tile_section(text: &mut String, section_name: &str, tile: &SetTile) {
934    write_section_header(text, section_name);
935    write_string_value(text, "Model", tile.model.as_ref());
936    write_string_value(text, "WalkMesh", tile.walkmesh.as_ref());
937    write_string_value(text, "TopLeft", tile.top_left.terrain.as_ref());
938    write_i32_value(text, "TopLeftHeight", tile.top_left.height);
939    write_string_value(text, "TopRight", tile.top_right.terrain.as_ref());
940    write_i32_value(text, "TopRightHeight", tile.top_right.height);
941    write_string_value(text, "BottomLeft", tile.bottom_left.terrain.as_ref());
942    write_i32_value(text, "BottomLeftHeight", tile.bottom_left.height);
943    write_string_value(text, "BottomRight", tile.bottom_right.terrain.as_ref());
944    write_i32_value(text, "BottomRightHeight", tile.bottom_right.height);
945    write_string_value(text, "Top", tile.edge_crossers.top.as_ref());
946    write_string_value(text, "Right", tile.edge_crossers.right.as_ref());
947    write_string_value(text, "Bottom", tile.edge_crossers.bottom.as_ref());
948    write_string_value(text, "Left", tile.edge_crossers.left.as_ref());
949    write_bool_value(text, "MainLight1", tile.main_light_1);
950    write_bool_value(text, "MainLight2", tile.main_light_2);
951    write_bool_value(text, "SourceLight1", tile.source_light_1);
952    write_bool_value(text, "SourceLight2", tile.source_light_2);
953    write_bool_value(text, "AnimLoop1", tile.anim_loop_1);
954    write_bool_value(text, "AnimLoop2", tile.anim_loop_2);
955    write_bool_value(text, "AnimLoop3", tile.anim_loop_3);
956    write_u32_value(text, "Doors", tile.doors);
957    write_u32_value(text, "Sounds", tile.sounds);
958    write_string_value(text, "PathNode", tile.path_node.as_ref());
959    write_i32_value(text, "Orientation", tile.orientation);
960    write_string_value(text, "VisibilityNode", tile.visibility_node.as_ref());
961    write_i32_value(text, "VisibilityOrientation", tile.visibility_orientation);
962    write_string_value(
963        text,
964        "DoorVisibilityNode",
965        tile.door_visibility_node.as_ref(),
966    );
967    write_i32_value(
968        text,
969        "DoorVisibilityOrientation",
970        tile.door_visibility_orientation,
971    );
972    write_string_value(text, "ImageMap2D", tile.image_map_2d.as_ref());
973}
974
975fn write_tile_door_section(text: &mut String, section_name: &str, tile_door: &SetTileDoor) {
976    write_section_header(text, section_name);
977    write_i32_value(text, "Type", tile_door.door_type);
978    write_f32_value(text, "X", tile_door.x);
979    write_f32_value(text, "Y", tile_door.y);
980    write_f32_value(text, "Z", tile_door.z);
981    write_i32_value(text, "Orientation", tile_door.orientation);
982}
983
984fn write_group_section(text: &mut String, section_name: &str, group: &SetGroup) {
985    write_section_header(text, section_name);
986    write_string_value(text, "Name", group.name.as_ref());
987    write_i32_value(text, "StrRef", group.str_ref);
988    write_u32_value(text, "Rows", group.rows);
989    write_u32_value(text, "Columns", group.columns);
990    for (index, tile_id) in &group.tiles {
991        text.push_str("Tile");
992        text.push_str(&index.to_string());
993        text.push('=');
994        match tile_id {
995            Some(tile_id) => text.push_str(&tile_id.to_string()),
996            None => text.push_str("-1"),
997        }
998        text.push('\n');
999    }
1000}
1001
1002/// Common imports for consumers of this crate.
1003pub mod prelude {
1004    pub use crate::{
1005        SET_RES_TYPE, SetError, SetFile, SetGeneral, SetGrass, SetGroup, SetNamedType,
1006        SetPrimaryRule, SetResult, SetTile, SetTileCorner, SetTileDoor, SetTileEdges,
1007        build_set_text, parse_set, read_set, write_set,
1008    };
1009}
1010
1011#[allow(clippy::panic)]
1012#[cfg(test)]
1013mod tests {
1014    use std::{fs, path::PathBuf};
1015
1016    use super::{SetFile, build_set_text, parse_set, write_set};
1017
1018    #[test]
1019    fn parses_minimal_tileset() {
1020        let parsed = parse_set(
1021            r#"
1022                [GENERAL]
1023                Name=TST01
1024                Type=SET
1025                Version=V1.0
1026                Interior=0
1027
1028                [TERRAIN TYPES]
1029                Count=1
1030
1031                [TERRAIN0]
1032                Name=Grass
1033                StrRef=42
1034
1035                [TILES]
1036                Count=1
1037
1038                [TILE0]
1039                Model=tst01_a01_01
1040                WalkMesh=msb01
1041                TopLeft=Grass
1042                TopLeftHeight=0
1043                TopRight=Grass
1044                TopRightHeight=0
1045                BottomLeft=Grass
1046                BottomLeftHeight=0
1047                BottomRight=Grass
1048                BottomRightHeight=0
1049                PathNode=A
1050                Orientation=90
1051            "#,
1052        )
1053        .unwrap_or_else(|error| panic!("parse set: {error}"));
1054
1055        assert_eq!(parsed.general.name.as_deref(), Some("TST01"));
1056        assert_eq!(
1057            parsed
1058                .terrains
1059                .get(&0)
1060                .and_then(|terrain| terrain.name.as_deref()),
1061            Some("Grass")
1062        );
1063        assert_eq!(
1064            parsed.tiles.get(&0).and_then(|tile| tile.model.as_deref()),
1065            Some("tst01_a01_01")
1066        );
1067        assert_eq!(
1068            parsed.tiles.get(&0).and_then(|tile| tile.orientation),
1069            Some(90)
1070        );
1071    }
1072
1073    #[test]
1074    fn parses_workspace_set_samples() {
1075        let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../set");
1076        if !root.is_dir() {
1077            return;
1078        }
1079        let entries = fs::read_dir(&root).unwrap_or_else(|error| {
1080            panic!("read set sample dir {}: {error}", root.display());
1081        });
1082
1083        let mut parsed_files = 0_usize;
1084        for entry in entries {
1085            let entry = entry.unwrap_or_else(|error| panic!("read dir entry: {error}"));
1086            let path = entry.path();
1087            if path.extension().and_then(|ext| ext.to_str()) != Some("set") {
1088                continue;
1089            }
1090
1091            let parsed = SetFile::from_file(&path).unwrap_or_else(|error| {
1092                panic!("parse {}: {error}", path.display());
1093            });
1094            assert!(
1095                !parsed.tiles.is_empty(),
1096                "expected at least one tile in {}",
1097                path.display()
1098            );
1099            parsed_files += 1;
1100        }
1101
1102        assert!(parsed_files > 0, "expected at least one sample .set file");
1103    }
1104
1105    #[test]
1106    fn builds_and_reparses_structured_tileset() {
1107        let original = parse_set(
1108            r#"
1109                [GENERAL]
1110                Name=TST01
1111                Type=SET
1112                Version=V1.0
1113                Interior=0
1114                HasHeightTransition=1
1115
1116                [GRASS]
1117                Grass=1
1118                GrassTextureName=grass01
1119                Density=1.5
1120                Height=2
1121                AmbientRed=0.1
1122                AmbientGreen=0.2
1123                AmbientBlue=0.3
1124                DiffuseRed=0.4
1125                DiffuseGreen=0.5
1126                DiffuseBlue=0.6
1127
1128                [TERRAIN TYPES]
1129                Count=1
1130
1131                [TERRAIN0]
1132                Name=Grass
1133                StrRef=42
1134
1135                [CROSSER TYPES]
1136                Count=1
1137
1138                [CROSSER0]
1139                Name=Road
1140
1141                [PRIMARY RULES]
1142                Count=1
1143
1144                [PRIMARY RULE0]
1145                Placed=Grass
1146                PlacedHeight=0
1147                Adjacent=Road
1148                AdjacentHeight=1
1149                Changed=Road
1150                ChangedHeight=2
1151
1152                [TILES]
1153                Count=1
1154
1155                [TILE0]
1156                Model=tst01_a01_01
1157                WalkMesh=msb01
1158                TopLeft=Grass
1159                TopLeftHeight=0
1160                TopRight=Grass
1161                TopRightHeight=1
1162                BottomLeft=Grass
1163                BottomLeftHeight=2
1164                BottomRight=Grass
1165                BottomRightHeight=3
1166                Top=Road
1167                Right=Road
1168                Bottom=Road
1169                Left=Road
1170                MainLight1=1
1171                SourceLight2=0
1172                AnimLoop3=1
1173                Doors=1
1174                Sounds=2
1175                PathNode=A
1176                Orientation=90
1177                VisibilityNode=V
1178                VisibilityOrientation=180
1179                DoorVisibilityNode=D
1180                DoorVisibilityOrientation=270
1181                ImageMap2D=tile0
1182
1183                [TILE0DOOR0]
1184                Type=3
1185                X=1
1186                Y=2
1187                Z=3
1188                Orientation=45
1189
1190                [GROUPS]
1191                Count=1
1192
1193                [GROUP0]
1194                Name=Corner
1195                StrRef=7
1196                Rows=1
1197                Columns=2
1198                Tile0=0
1199                Tile1=-1
1200            "#,
1201        )
1202        .unwrap_or_else(|error| panic!("parse set: {error}"));
1203
1204        let built = build_set_text(&original).unwrap_or_else(|error| panic!("build set: {error}"));
1205        assert!(built.contains("[TERRAIN TYPES]\nCount=1"));
1206        assert!(built.contains("[GROUPS]\nCount=1"));
1207
1208        let reparsed = parse_set(&built).unwrap_or_else(|error| panic!("reparse set: {error}"));
1209        assert_eq!(reparsed, original);
1210    }
1211
1212    #[test]
1213    fn write_set_matches_build_text() {
1214        let original = parse_set(
1215            r#"
1216                [GENERAL]
1217                Name=TST01
1218
1219                [TILES]
1220                Count=1
1221
1222                [TILE0]
1223                Model=tst01_a01_01
1224            "#,
1225        )
1226        .unwrap_or_else(|error| panic!("parse set: {error}"));
1227
1228        let built = build_set_text(&original).unwrap_or_else(|error| panic!("build set: {error}"));
1229        let mut bytes = Vec::new();
1230        write_set(&mut bytes, &original).unwrap_or_else(|error| panic!("write set: {error}"));
1231
1232        let written =
1233            String::from_utf8(bytes).unwrap_or_else(|error| panic!("utf8 write set: {error}"));
1234        assert_eq!(written, built);
1235        let reparsed = parse_set(&written).unwrap_or_else(|error| panic!("reparse set: {error}"));
1236        assert_eq!(reparsed, original);
1237    }
1238
1239    #[test]
1240    fn rejects_building_tileset_without_tiles() {
1241        let error = build_set_text(&SetFile::default())
1242            .err()
1243            .unwrap_or_else(|| panic!("expected build error for empty tileset"));
1244        assert_eq!(
1245            error.to_string(),
1246            "cannot build SET payload without at least one tile definition"
1247        );
1248    }
1249}