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
17pub const SET_RES_TYPE: ResType = ResType(2013);
19
20#[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 Io(io::Error),
31 ResMan(ResManError),
33 Message(String),
35}
36
37impl SetError {
38 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
74pub type SetResult<T> = Result<T, SetError>;
76
77#[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 pub general: SetGeneral,
94 pub grass: Option<SetGrass>,
96 pub terrains: BTreeMap<u32, SetNamedType>,
98 pub crossers: BTreeMap<u32, SetNamedType>,
100 pub primary_rules: BTreeMap<u32, SetPrimaryRule>,
102 pub tiles: BTreeMap<u32, SetTile>,
104 pub tile_doors: BTreeMap<(u32, u32), SetTileDoor>,
106 pub groups: BTreeMap<u32, SetGroup>,
108}
109
110impl SetFile {
111 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 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 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#[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 pub name: Option<String>,
186 pub file_type: Option<String>,
188 pub version: Option<String>,
190 pub interior: Option<bool>,
192 pub has_height_transition: Option<bool>,
194 pub env_map: Option<String>,
196 pub transition: Option<i32>,
198 pub selector_height: Option<i32>,
200 pub display_name: Option<i32>,
202 pub unlocalized_name: Option<String>,
204 pub border: Option<String>,
206 pub default_terrain: Option<String>,
208 pub floor: Option<String>,
210}
211
212#[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 pub grass: Option<bool>,
224 pub texture_name: Option<String>,
226 pub density: Option<f32>,
228 pub height: Option<f32>,
230 pub ambient: Option<[f32; 3]>,
232 pub diffuse: Option<[f32; 3]>,
234}
235
236#[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 pub id: u32,
248 pub name: Option<String>,
250 pub str_ref: Option<i32>,
252}
253
254#[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 pub terrain: Option<String>,
266 pub height: Option<i32>,
268}
269
270#[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 pub top: Option<String>,
282 pub right: Option<String>,
284 pub bottom: Option<String>,
286 pub left: Option<String>,
288}
289
290#[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 pub id: u32,
302 pub model: Option<String>,
304 pub walkmesh: Option<String>,
306 pub top_left: SetTileCorner,
308 pub top_right: SetTileCorner,
310 pub bottom_left: SetTileCorner,
312 pub bottom_right: SetTileCorner,
314 pub edge_crossers: SetTileEdges,
316 pub main_light_1: Option<bool>,
318 pub main_light_2: Option<bool>,
320 pub source_light_1: Option<bool>,
322 pub source_light_2: Option<bool>,
324 pub anim_loop_1: Option<bool>,
326 pub anim_loop_2: Option<bool>,
328 pub anim_loop_3: Option<bool>,
330 pub doors: Option<u32>,
332 pub sounds: Option<u32>,
334 pub path_node: Option<String>,
336 pub orientation: Option<i32>,
338 pub visibility_node: Option<String>,
340 pub visibility_orientation: Option<i32>,
342 pub door_visibility_node: Option<String>,
344 pub door_visibility_orientation: Option<i32>,
346 pub image_map_2d: Option<String>,
348}
349
350#[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 pub tile_id: u32,
362 pub door_id: u32,
364 pub door_type: Option<i32>,
366 pub x: Option<f32>,
368 pub y: Option<f32>,
370 pub z: Option<f32>,
372 pub orientation: Option<i32>,
374}
375
376#[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 pub id: u32,
388 pub name: Option<String>,
390 pub str_ref: Option<i32>,
392 pub rows: Option<u32>,
394 pub columns: Option<u32>,
396 pub tiles: BTreeMap<u32, Option<u32>>,
398}
399
400#[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 pub id: u32,
412 pub placed: Option<String>,
414 pub placed_height: Option<i32>,
416 pub adjacent: Option<String>,
418 pub adjacent_height: Option<i32>,
420 pub changed: Option<String>,
422 pub changed_height: Option<i32>,
424}
425
426#[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
445pub 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, ¤t_section, ¤t_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, ¤t_section, ¤t_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
495pub 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
568pub 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(§ion_upper, "TERRAIN") {
596 set_file
597 .terrains
598 .insert(index, parse_named_type(index, entries));
599 } else if let Some(index) = parse_indexed_section(§ion_upper, "CROSSER") {
600 set_file
601 .crossers
602 .insert(index, parse_named_type(index, entries));
603 } else if let Some(index) = parse_indexed_section(§ion_upper, "GROUP") {
604 set_file.groups.insert(index, parse_group(index, entries));
605 } else if let Some(index) = parse_indexed_section(§ion_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(§ion_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(§ion_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
1002pub 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}