From 2b05f224034d16d00c99fc824c62152181df103d Mon Sep 17 00:00:00 2001 From: JToTheThree Date: Thu, 5 Dec 2024 21:07:20 -0600 Subject: [PATCH 01/21] add world file parsing --- Cargo.toml | 7 +- assets/world/map-x00-y00-desert.tmx | 1 + assets/world/map-x01-y01-plains.tmx | 1 + assets/world/world_basic.world | 20 +++ assets/world/world_pattern.world | 12 ++ assets/world/world_pattern_bad.world | 12 ++ src/error.rs | 3 + src/lib.rs | 2 + src/loader.rs | 7 +- src/world.rs | 214 +++++++++++++++++++++++++++ tests/lib.rs | 43 ++++++ 11 files changed, 317 insertions(+), 5 deletions(-) create mode 100644 assets/world/map-x00-y00-desert.tmx create mode 100644 assets/world/map-x01-y01-plains.tmx create mode 100644 assets/world/world_basic.world create mode 100644 assets/world/world_pattern.world create mode 100644 assets/world/world_pattern_bad.world create mode 100644 src/world.rs diff --git a/Cargo.toml b/Cargo.toml index d7ee6487..ca07ce55 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,10 +23,6 @@ path = "src/lib.rs" name = "example" path = "examples/main.rs" -[[example]] -name = "sfml" -path = "examples/sfml/main.rs" - [[example]] name = "ggez" path = "examples/ggez/main.rs" @@ -36,6 +32,9 @@ base64 = "0.22.1" xml-rs = "0.8.4" zstd = { version = "0.13.1", optional = true, default-features = false } flate2 = "1.0.28" +serde = "1.0.215" +serde_json = "1.0.133" +regex = "1.11.1" [dev-dependencies.sfml] version = "0.21.0" diff --git a/assets/world/map-x00-y00-desert.tmx b/assets/world/map-x00-y00-desert.tmx new file mode 100644 index 00000000..c4c14ba9 --- /dev/null +++ b/assets/world/map-x00-y00-desert.tmx @@ -0,0 +1 @@ +Empty Test File \ No newline at end of file diff --git a/assets/world/map-x01-y01-plains.tmx b/assets/world/map-x01-y01-plains.tmx new file mode 100644 index 00000000..c4c14ba9 --- /dev/null +++ b/assets/world/map-x01-y01-plains.tmx @@ -0,0 +1 @@ +Empty Test File \ No newline at end of file diff --git a/assets/world/world_basic.world b/assets/world/world_basic.world new file mode 100644 index 00000000..c42eab81 --- /dev/null +++ b/assets/world/world_basic.world @@ -0,0 +1,20 @@ +{ + "maps": [ + { + "fileName": "map01.tmx", + "height": 640, + "width": 960, + "x": 0, + "y": 0 + }, + { + "fileName": "map02.tmx", + "height": 640, + "width": 960, + "x": 960, + "y": 0 + } + ], + "onlyShowAdjacentMaps": false, + "type": "world" +} diff --git a/assets/world/world_pattern.world b/assets/world/world_pattern.world new file mode 100644 index 00000000..50c604b5 --- /dev/null +++ b/assets/world/world_pattern.world @@ -0,0 +1,12 @@ +{ + "patterns": [ + { + "regexp": "map-x0*(\\d+)-y0*(\\d+)-.*\\.tmx", + "multiplierX": 640, + "multiplierY": 480, + "offsetX": 240, + "offsetY": -240 + } + ], + "type": "world" +} \ No newline at end of file diff --git a/assets/world/world_pattern_bad.world b/assets/world/world_pattern_bad.world new file mode 100644 index 00000000..c40ed348 --- /dev/null +++ b/assets/world/world_pattern_bad.world @@ -0,0 +1,12 @@ +{ + "patterns": [ + { + "regexp": "map-x0*(\\\\dddd+)-y0*(\\\\dffd+)-.\\.tmx", + "multiplierX": 12000000, + "multiplierY": 48000000000000000000000000000000000000, + "offsetX": 240, + "offsetY": -240 + } + ], + "type": "world" +} \ No newline at end of file diff --git a/src/error.rs b/src/error.rs index e1d1b4b0..08f31bb0 100644 --- a/src/error.rs +++ b/src/error.rs @@ -57,6 +57,8 @@ pub enum Error { CsvDecodingError(CsvDecodingError), /// An error occurred when parsing an XML file, such as a TMX or TSX file. XmlDecodingError(xml::reader::Error), + /// An error occurred when attempting to deserialize a JSON file. + JsonDecodingError(serde_json::Error), /// The XML stream ended before the document was fully parsed. PrematureEnd(String), /// The path given is invalid because it isn't contained in any folder. @@ -120,6 +122,7 @@ impl fmt::Display for Error { Error::Base64DecodingError(e) => write!(fmt, "{}", e), Error::CsvDecodingError(e) => write!(fmt, "{}", e), Error::XmlDecodingError(e) => write!(fmt, "{}", e), + Error::JsonDecodingError(e) => write!(fmt, "{}", e), Error::PrematureEnd(e) => write!(fmt, "{}", e), Error::PathIsNotFile => { write!( diff --git a/src/lib.rs b/src/lib.rs index fa68ac9e..0a3b38d3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,6 +20,7 @@ mod template; mod tile; mod tileset; mod util; +mod world; pub use animation::*; pub use cache::*; @@ -34,3 +35,4 @@ pub use reader::*; pub use template::*; pub use tile::*; pub use tileset::*; +pub use world::*; diff --git a/src/loader.rs b/src/loader.rs index ad4dc6eb..71f90512 100644 --- a/src/loader.rs +++ b/src/loader.rs @@ -2,7 +2,7 @@ use std::path::Path; use crate::{ DefaultResourceCache, FilesystemResourceReader, Map, ResourceCache, ResourceReader, Result, - Tileset, + Tileset, World, }; /// A type used for loading [`Map`]s and [`Tileset`]s. @@ -182,6 +182,11 @@ impl Loader { crate::parse::xml::parse_tileset(path.as_ref(), &mut self.reader, &mut self.cache) } + /// Parses a file hopefully containing a Tiled world and tries to parse it. All external files + pub fn load_world(&mut self, path: impl AsRef) -> Result { + crate::world::parse_world(path.as_ref()) + } + /// Returns a reference to the loader's internal [`ResourceCache`]. pub fn cache(&self) -> &Cache { &self.cache diff --git a/src/world.rs b/src/world.rs new file mode 100644 index 00000000..c0a12653 --- /dev/null +++ b/src/world.rs @@ -0,0 +1,214 @@ +use std::{ + fs, + path::{Path, PathBuf}, +}; + +use regex::Regex; +use serde::Deserialize; + +use crate::Error; + +/// A World is a collection of maps and their layout in the game world. +#[derive(Deserialize, PartialEq, Clone, Debug)] +pub struct World { + /// The path first used in a ['ResourceReader'] to load this world. + #[serde(skip_deserializing)] + pub source: PathBuf, + /// The maps present in this world. + pub maps: Option>, + /// Optional regex pattern to load maps. + patterns: Option>, + /// The type of world, which is arbitrary and set by the user. + #[serde(rename = "type")] + pub world_type: Option, +} + +/// A WorldMap provides the information for a map in the world and its layout. +#[derive(Deserialize, PartialEq, Clone, Debug)] +pub struct WorldMap { + /// The filename of the tmx map. + #[serde(rename = "fileName")] + pub filename: String, + /// The x position of the map. + pub x: i32, + /// The y position of the map. + pub y: i32, + /// The optional width of the map. + pub width: Option, + /// The optional height of the map. + pub height: Option, +} + +/// A WorldPattern defines a regex pattern to automatically determine which maps to load and how to lay them out. +#[derive(Deserialize, PartialEq, Clone, Debug)] +struct WorldPattern { + /// The regex pattern to match against filenames. The first two capture groups should be the x integer and y integer positions. + pub regexp: String, + /// The multiplier for the x position. + #[serde(rename = "multiplierX")] + pub multiplier_x: u32, + /// The multiplier for the y position. + #[serde(rename = "multiplierY")] + pub multiplier_y: u32, + /// The offset for the x position. + #[serde(rename = "offsetX")] + pub offset_x: i32, + /// The offset for the y position. + #[serde(rename = "offsetY")] + pub offset_y: i32, +} + +/// Parse a Tiled World file from a path. +/// If a the Patterns field is present, it will attempt to build the maps list based on the regex patterns. +/// +/// ## Example +/// ``` +/// # use tiled::Loader; +/// # +/// # fn main() { +/// # let loader = Loader::new(); +/// # let world = loader.load_world("world.world").unwrap(); +/// # +/// # for map in world.maps.unwrap() { +/// # println!("Map: {:?}", map); +/// # } +/// # } +/// ``` +pub(crate) fn parse_world(path: &Path) -> Result { + let world_file = match std::fs::read_to_string(path) { + Ok(world_file) => world_file, + Err(err) => { + return Err(Error::ResourceLoadingError { + path: path.to_owned(), + err: Box::new(err), + }) + } + }; + + let mut world: World = match serde_json::from_str(&world_file) { + Ok(world) => world, + Err(err) => { + return Err(Error::JsonDecodingError(err)); + } + }; + + if world.patterns.is_some() { + world.maps = match parse_world_pattern(path, &world.clone().patterns.unwrap()) { + Ok(maps) => Some(maps), + Err(err) => return Err(err), + }; + } + + Ok(world) +} + +/// If "patterns" key is present, it will attempt to build the maps list based on the regex patterns. +fn parse_world_pattern(path: &Path, patterns: &Vec) -> Result, Error> { + let mut maps = Vec::new(); + + let parent_dir = path.parent().ok_or(Error::ResourceLoadingError { + path: path.to_owned(), + err: Box::new(std::io::Error::from(std::io::ErrorKind::NotFound)), + })?; + + // There's no documentation on why "patterns" is a JSON array, so we'll just blast them into same maps list. + for pattern in patterns { + let files = fs::read_dir(parent_dir).map_err(|err| Error::ResourceLoadingError { + path: parent_dir.to_owned(), + err: Box::new(err), + })?; + + let re = Regex::new(&pattern.regexp).unwrap(); + let files = files + .filter_map(|entry| entry.ok()) + .filter(|entry| re.is_match(entry.path().file_name().unwrap().to_str().unwrap())) + .map(|entry| { + let filename = entry + .path() + .file_name() + .ok_or_else(|| Error::ResourceLoadingError { + path: path.to_owned(), + err: "Failed to get file name".into(), + })? + .to_str() + .ok_or_else(|| Error::ResourceLoadingError { + path: path.to_owned(), + err: "Failed to convert file name to string".into(), + })? + .to_owned(); + + let captures = re.captures(&filename).unwrap(); + + // let captures = + // re.captures(&filename) + // .ok_or_else(|| Error::ResourceLoadingError { + // path: path.to_owned(), + // err: format!("Failed checking regex match on file {}", filename).into(), + // })?; + + let x = captures + .get(1) + .ok_or_else(|| Error::ResourceLoadingError { + path: path.to_owned(), + err: format!("Failed to parse x pattern from file {}", filename).into(), + })? + .as_str() + .parse::() + .map_err(|e| Error::ResourceLoadingError { + path: path.to_owned(), + err: Box::new(e), + })?; + + let x = match x + .checked_mul(pattern.multiplier_x as i32) + .and_then(|x| x.checked_add(pattern.offset_x)) + { + Some(x) => x, + None => { + return Err(Error::ResourceLoadingError { + path: path.to_owned(), + err: "Arithmetic Overflow on multiplierX and offsetX".into(), + }) + } + }; + let y = captures + .get(2) + .ok_or_else(|| Error::ResourceLoadingError { + path: path.to_owned(), + err: format!("Failed to parse y pattern from file {}", filename).into(), + })? + .as_str() + .parse::() + .map_err(|e| Error::ResourceLoadingError { + path: path.to_owned(), + err: Box::new(e), + })?; + let y = match y + .checked_mul(pattern.multiplier_y as i32) + .and_then(|y| y.checked_add(pattern.offset_y)) + { + Some(y) => y, + None => { + return Err(Error::ResourceLoadingError { + path: path.to_owned(), + err: "Arithmetic Overflow on multiplierY and offsetY".into(), + }) + } + }; + Ok(WorldMap { + filename, + x, + y, + width: Some(pattern.multiplier_x), + height: Some(pattern.multiplier_y), + }) + }) + .collect::>(); + + for file in files { + maps.push(file?); + } + } + + Ok(maps) +} diff --git a/tests/lib.rs b/tests/lib.rs index a07c66ad..1a868461 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -68,6 +68,49 @@ fn test_external_tileset() { compare_everything_but_sources(&r, &e); } +#[test] +fn test_loading_world() { + let mut loader = Loader::new(); + + let e = loader.load_world("assets/world/world_basic.world").unwrap(); + + let maps = e.maps.unwrap(); + + assert_eq!(e.world_type.unwrap(), "world"); + assert_eq!(maps[0].filename, "map01.tmx"); + assert_eq!(maps[1].x, 960); + assert_eq!(maps[1].y, 0); + assert_eq!(maps[1].width, Some(960)); + assert_eq!(maps[1].height, Some(640)); + assert_eq!(maps.len(), 2); +} + +#[test] +fn test_loading_world_pattern() { + let mut loader = Loader::new(); + + let e = loader.load_world("assets/world/world_pattern.world").unwrap(); + + let maps = e.maps.unwrap(); + + assert_eq!(e.world_type.unwrap(), "world"); + assert_eq!(maps[0].filename, "map-x01-y01-plains.tmx"); + assert_eq!(maps[0].x, 880); + assert_eq!(maps[0].y, 240); + assert_eq!(maps[1].filename, "map-x00-y00-desert.tmx"); +} + +#[test] +fn test_bad_loading_world_pattern() { + let mut loader = Loader::new(); + + let e = loader.load_world("assets/world/world_bad_pattern.world"); + assert!(e.is_err()); + + let e = loader.load_world("assets/world/world_pattern_bad.world"); + assert!(e.is_err()); +} + #[test] fn test_cache() { let mut loader = Loader::new(); From 0eaed1eeb1f284cd511925236fe5dc34875511a6 Mon Sep 17 00:00:00 2001 From: JToTheThree Date: Thu, 5 Dec 2024 21:13:00 -0600 Subject: [PATCH 02/21] add world file parsing --- Cargo.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index ca07ce55..d56d191f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,10 @@ path = "src/lib.rs" name = "example" path = "examples/main.rs" +[[example]] +name = "sfml" +path = "examples/sfml/main.rs" + [[example]] name = "ggez" path = "examples/ggez/main.rs" From ed6cfa3ae3196322aa22e60d29ed215c89ebdd36 Mon Sep 17 00:00:00 2001 From: JToTheThree Date: Thu, 12 Dec 2024 10:29:50 -0600 Subject: [PATCH 03/21] clarity and cleanup --- .../{map-x00-y00-desert.tmx => map-x00-y00-empty.tmx} | 0 .../{map-x01-y01-plains.tmx => map-x01-y01-empty.tmx} | 0 src/world.rs | 7 ------- tests/lib.rs | 4 ++-- 4 files changed, 2 insertions(+), 9 deletions(-) rename assets/world/{map-x00-y00-desert.tmx => map-x00-y00-empty.tmx} (100%) rename assets/world/{map-x01-y01-plains.tmx => map-x01-y01-empty.tmx} (100%) diff --git a/assets/world/map-x00-y00-desert.tmx b/assets/world/map-x00-y00-empty.tmx similarity index 100% rename from assets/world/map-x00-y00-desert.tmx rename to assets/world/map-x00-y00-empty.tmx diff --git a/assets/world/map-x01-y01-plains.tmx b/assets/world/map-x01-y01-empty.tmx similarity index 100% rename from assets/world/map-x01-y01-plains.tmx rename to assets/world/map-x01-y01-empty.tmx diff --git a/src/world.rs b/src/world.rs index c0a12653..7d5dc7f9 100644 --- a/src/world.rs +++ b/src/world.rs @@ -139,13 +139,6 @@ fn parse_world_pattern(path: &Path, patterns: &Vec) -> Result Date: Sun, 15 Dec 2024 15:00:38 -0600 Subject: [PATCH 04/21] read world from reader --- src/loader.rs | 2 +- src/world.rs | 30 ++++++++++++++++++++++-------- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/src/loader.rs b/src/loader.rs index 71f90512..8a7166a0 100644 --- a/src/loader.rs +++ b/src/loader.rs @@ -184,7 +184,7 @@ impl Loader { /// Parses a file hopefully containing a Tiled world and tries to parse it. All external files pub fn load_world(&mut self, path: impl AsRef) -> Result { - crate::world::parse_world(path.as_ref()) + crate::world::parse_world(path.as_ref(), &mut self.reader, &mut self.cache) } /// Returns a reference to the loader's internal [`ResourceCache`]. diff --git a/src/world.rs b/src/world.rs index 7d5dc7f9..9202fa87 100644 --- a/src/world.rs +++ b/src/world.rs @@ -1,12 +1,11 @@ use std::{ - fs, - path::{Path, PathBuf}, + fs, io::Read, path::{Path, PathBuf} }; use regex::Regex; use serde::Deserialize; -use crate::Error; +use crate::{Error, ResourceCache, ResourceReader}; /// A World is a collection of maps and their layout in the game world. #[derive(Deserialize, PartialEq, Clone, Debug)] @@ -74,8 +73,23 @@ struct WorldPattern { /// # } /// # } /// ``` -pub(crate) fn parse_world(path: &Path) -> Result { - let world_file = match std::fs::read_to_string(path) { +pub(crate) fn parse_world( + world_path: &Path, + reader: &mut impl ResourceReader, + cache: &mut impl ResourceCache, +) -> Result { + let mut path = reader.read_from(&world_path).map_err(|err| Error::ResourceLoadingError { + path: world_path.to_owned(), + err: Box::new(err), + })?; + + let mut world_string = String::new(); + path.read_to_string(&mut world_string).map_err(|err| Error::ResourceLoadingError { + path: world_path.to_owned(), + err: Box::new(err), + })?; + + /*let world_file = match std::fs::read_to_string(path) { Ok(world_file) => world_file, Err(err) => { return Err(Error::ResourceLoadingError { @@ -83,9 +97,9 @@ pub(crate) fn parse_world(path: &Path) -> Result { err: Box::new(err), }) } - }; + };*/ - let mut world: World = match serde_json::from_str(&world_file) { + let mut world: World = match serde_json::from_str(&world_string) { Ok(world) => world, Err(err) => { return Err(Error::JsonDecodingError(err)); @@ -93,7 +107,7 @@ pub(crate) fn parse_world(path: &Path) -> Result { }; if world.patterns.is_some() { - world.maps = match parse_world_pattern(path, &world.clone().patterns.unwrap()) { + world.maps = match parse_world_pattern(world_path, &world.clone().patterns.unwrap()) { Ok(maps) => Some(maps), Err(err) => return Err(err), }; From 8a57dccd8a40beae155fb707765fd55673961897 Mon Sep 17 00:00:00 2001 From: JToTheThree Date: Sun, 15 Dec 2024 19:54:38 -0600 Subject: [PATCH 05/21] add option to preload maps in world --- src/loader.rs | 7 +++---- src/world.rs | 26 +++++++++++++++----------- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/loader.rs b/src/loader.rs index 8a7166a0..319bd0f8 100644 --- a/src/loader.rs +++ b/src/loader.rs @@ -1,8 +1,7 @@ use std::path::Path; use crate::{ - DefaultResourceCache, FilesystemResourceReader, Map, ResourceCache, ResourceReader, Result, - Tileset, World, + DefaultResourceCache, FilesystemResourceReader, Map, ResourceCache, ResourceReader, Result, Tileset, World }; /// A type used for loading [`Map`]s and [`Tileset`]s. @@ -183,8 +182,8 @@ impl Loader { } /// Parses a file hopefully containing a Tiled world and tries to parse it. All external files - pub fn load_world(&mut self, path: impl AsRef) -> Result { - crate::world::parse_world(path.as_ref(), &mut self.reader, &mut self.cache) + pub fn load_world(&mut self, path: impl AsRef, load_maps: bool) -> Result { + crate::world::parse_world(path.as_ref(), load_maps, &mut self.reader, &mut self.cache) } /// Returns a reference to the loader's internal [`ResourceCache`]. diff --git a/src/world.rs b/src/world.rs index 9202fa87..1132b5eb 100644 --- a/src/world.rs +++ b/src/world.rs @@ -5,7 +5,7 @@ use std::{ use regex::Regex; use serde::Deserialize; -use crate::{Error, ResourceCache, ResourceReader}; +use crate::{Error, Map, ResourceCache, ResourceReader}; /// A World is a collection of maps and their layout in the game world. #[derive(Deserialize, PartialEq, Clone, Debug)] @@ -28,6 +28,9 @@ pub struct WorldMap { /// The filename of the tmx map. #[serde(rename = "fileName")] pub filename: String, + /// Map Data + #[serde(skip_deserializing)] + pub map: Option, /// The x position of the map. pub x: i32, /// The y position of the map. @@ -75,6 +78,7 @@ struct WorldPattern { /// ``` pub(crate) fn parse_world( world_path: &Path, + load_maps: bool, reader: &mut impl ResourceReader, cache: &mut impl ResourceCache, ) -> Result { @@ -89,16 +93,6 @@ pub(crate) fn parse_world( err: Box::new(err), })?; - /*let world_file = match std::fs::read_to_string(path) { - Ok(world_file) => world_file, - Err(err) => { - return Err(Error::ResourceLoadingError { - path: path.to_owned(), - err: Box::new(err), - }) - } - };*/ - let mut world: World = match serde_json::from_str(&world_string) { Ok(world) => world, Err(err) => { @@ -113,6 +107,15 @@ pub(crate) fn parse_world( }; } + if load_maps { + if let Some(maps) = &mut world.maps { + for map in maps.iter_mut() { + let map_path = world_path.with_file_name(&map.filename); + map.map = Some(crate::parse::xml::parse_map(&map_path, reader, cache)?); + } + } + } + Ok(world) } @@ -204,6 +207,7 @@ fn parse_world_pattern(path: &Path, patterns: &Vec) -> Result Date: Sun, 15 Dec 2024 21:03:25 -0600 Subject: [PATCH 06/21] fix documentation, revert auto load maps --- src/loader.rs | 6 +++--- src/world.rs | 17 +++++------------ tests/lib.rs | 5 +---- 3 files changed, 9 insertions(+), 19 deletions(-) diff --git a/src/loader.rs b/src/loader.rs index 319bd0f8..8bb53e7b 100644 --- a/src/loader.rs +++ b/src/loader.rs @@ -181,9 +181,9 @@ impl Loader { crate::parse::xml::parse_tileset(path.as_ref(), &mut self.reader, &mut self.cache) } - /// Parses a file hopefully containing a Tiled world and tries to parse it. All external files - pub fn load_world(&mut self, path: impl AsRef, load_maps: bool) -> Result { - crate::world::parse_world(path.as_ref(), load_maps, &mut self.reader, &mut self.cache) + /// Parses a file hopefully containing a Tiled world and tries to parse it. + pub fn load_world(&mut self, path: impl AsRef) -> Result { + crate::world::parse_world(path.as_ref(), &mut self.reader, &mut self.cache) } /// Returns a reference to the loader's internal [`ResourceCache`]. diff --git a/src/world.rs b/src/world.rs index 1132b5eb..58912b6d 100644 --- a/src/world.rs +++ b/src/world.rs @@ -63,13 +63,16 @@ struct WorldPattern { /// Parse a Tiled World file from a path. /// If a the Patterns field is present, it will attempt to build the maps list based on the regex patterns. /// +/// ## Parameters +/// - `world_path`: The path to the world file. +/// /// ## Example /// ``` /// # use tiled::Loader; /// # /// # fn main() { -/// # let loader = Loader::new(); -/// # let world = loader.load_world("world.world").unwrap(); +/// # let mut loader = Loader::new(); +/// # let world = loader.load_world("assets/world/world_basic.world").unwrap(); /// # /// # for map in world.maps.unwrap() { /// # println!("Map: {:?}", map); @@ -78,7 +81,6 @@ struct WorldPattern { /// ``` pub(crate) fn parse_world( world_path: &Path, - load_maps: bool, reader: &mut impl ResourceReader, cache: &mut impl ResourceCache, ) -> Result { @@ -107,15 +109,6 @@ pub(crate) fn parse_world( }; } - if load_maps { - if let Some(maps) = &mut world.maps { - for map in maps.iter_mut() { - let map_path = world_path.with_file_name(&map.filename); - map.map = Some(crate::parse::xml::parse_map(&map_path, reader, cache)?); - } - } - } - Ok(world) } diff --git a/tests/lib.rs b/tests/lib.rs index 8e248983..e983c5a8 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -94,10 +94,7 @@ fn test_loading_world_pattern() { let maps = e.maps.unwrap(); assert_eq!(e.world_type.unwrap(), "world"); - assert_eq!(maps[0].filename, "map-x01-y01-empty.tmx"); - assert_eq!(maps[0].x, 880); - assert_eq!(maps[0].y, 240); - assert_eq!(maps[1].filename, "map-x00-y00-empty.tmx"); + assert_eq!(maps.len(), 2); } #[test] From 505d6ad973fc33185191f75788d2cf44bd4ffca8 Mon Sep 17 00:00:00 2001 From: JToTheThree Date: Sun, 15 Dec 2024 21:07:34 -0600 Subject: [PATCH 07/21] remove tmx map variable from worldmap --- src/world.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/world.rs b/src/world.rs index 58912b6d..ea2c60a4 100644 --- a/src/world.rs +++ b/src/world.rs @@ -28,9 +28,6 @@ pub struct WorldMap { /// The filename of the tmx map. #[serde(rename = "fileName")] pub filename: String, - /// Map Data - #[serde(skip_deserializing)] - pub map: Option, /// The x position of the map. pub x: i32, /// The y position of the map. @@ -200,7 +197,6 @@ fn parse_world_pattern(path: &Path, patterns: &Vec) -> Result Date: Tue, 17 Dec 2024 23:42:21 -0600 Subject: [PATCH 08/21] Remove automatic dir pattern testing --- .github/workflows/rust.yml | 2 +- CHANGELOG.md | 4 + Cargo.toml | 9 +- README.md | 4 +- assets/world/map-x00-y00-empty.tmx | 1 - assets/world/map-x01-y01-empty.tmx | 1 - assets/world/world_pattern.world | 14 ++ assets/world/world_pattern_bad.world | 12 -- src/error.rs | 7 + src/lib.rs | 2 + src/loader.rs | 11 +- src/world.rs | 230 ++++++++++----------------- tests/lib.rs | 28 ++-- 13 files changed, 143 insertions(+), 182 deletions(-) delete mode 100644 assets/world/map-x00-y00-empty.tmx delete mode 100644 assets/world/map-x01-y01-empty.tmx delete mode 100644 assets/world/world_pattern_bad.world diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 8c665ccb..109da256 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -30,7 +30,7 @@ jobs: run: cargo build --lib --verbose - name: Run tests - run: cargo test --verbose + run: cargo test --verbose --all-features rustfmt: runs-on: ubuntu-24.04 diff --git a/CHANGELOG.md b/CHANGELOG.md index 237872ed..be1acfa4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.14.0] +### Added +- Added a new crate feature `world` to enable support for parsing `World` files. + ## [0.13.0] ### Added - Added a `source` member to `Tileset`, `Map` and `Template`, which stores the resource path they have been loaded from. (#303) diff --git a/Cargo.toml b/Cargo.toml index d56d191f..eeb00cb0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tiled" -version = "0.13.0" +version = "0.14.0" description = "A rust crate for loading maps created by the Tiled editor" categories = ["game-development"] keywords = ["gamedev", "tiled", "tmx", "map"] @@ -14,6 +14,7 @@ include = ["src/**/*.rs", "README.md", "LICENSE", "CHANGELOG.md"] [features] default = ["zstd"] wasm = ["zstd/wasm"] +world = ["serde", "serde_json", "regex"] [lib] name = "tiled" @@ -36,9 +37,9 @@ base64 = "0.22.1" xml-rs = "0.8.4" zstd = { version = "0.13.1", optional = true, default-features = false } flate2 = "1.0.28" -serde = "1.0.215" -serde_json = "1.0.133" -regex = "1.11.1" +serde = { version = "1.0.216", optional = true } +serde_json = { version = "1.0.133", optional = true } +regex = { version = "1.11.1", optional = true } [dev-dependencies.sfml] version = "0.21.0" diff --git a/README.md b/README.md index 62fb65bf..1f437fa2 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ let mut loader = Loader::with_reader( // Doing this embedding is useful for places where the OS filesystem is not available (e.g. WASM applications). |path: &std::path::Path| -> std::io::Result<_> { if path == std::path::Path::new("/my-map.tmx") { - Ok(std::io::Cursor::new(include_bytes!("../assets/tiled_csv.tmx"))) + Ok(std::io::Cursor::new(include_bytes!("assets/tiled_csv.tmx"))) } else { Err(std::io::ErrorKind::NotFound.into()) } @@ -86,7 +86,7 @@ impl tiled::ResourceReader for MyReader { // really dumb example implementation that just keeps resources in memory fn read_from(&mut self, path: &std::path::Path) -> std::result::Result { if path == std::path::Path::new("my_map.tmx") { - Ok(Cursor::new(include_bytes!("../assets/tiled_xml.tmx"))) + Ok(Cursor::new(include_bytes!("assets/tiled_xml.tmx"))) } else { Err(std::io::Error::new(std::io::ErrorKind::NotFound, "file not found")) } diff --git a/assets/world/map-x00-y00-empty.tmx b/assets/world/map-x00-y00-empty.tmx deleted file mode 100644 index c4c14ba9..00000000 --- a/assets/world/map-x00-y00-empty.tmx +++ /dev/null @@ -1 +0,0 @@ -Empty Test File \ No newline at end of file diff --git a/assets/world/map-x01-y01-empty.tmx b/assets/world/map-x01-y01-empty.tmx deleted file mode 100644 index c4c14ba9..00000000 --- a/assets/world/map-x01-y01-empty.tmx +++ /dev/null @@ -1 +0,0 @@ -Empty Test File \ No newline at end of file diff --git a/assets/world/world_pattern.world b/assets/world/world_pattern.world index 50c604b5..5e307baa 100644 --- a/assets/world/world_pattern.world +++ b/assets/world/world_pattern.world @@ -6,6 +6,20 @@ "multiplierY": 480, "offsetX": 240, "offsetY": -240 + }, + { + "regexp": "overworld-x0*(\\d+)-y0*(\\d+).tmx", + "multiplierX": 640, + "multiplierY": 480, + "offsetX": 4192, + "offsetY": 4192 + }, + { + "regexp": "OVERFLOW-x0*(\\d+)-y0*(\\d+).tmx", + "multiplierX": 50000000, + "multiplierY": 50000000, + "offsetX": 4192, + "offsetY": 4192 } ], "type": "world" diff --git a/assets/world/world_pattern_bad.world b/assets/world/world_pattern_bad.world deleted file mode 100644 index c40ed348..00000000 --- a/assets/world/world_pattern_bad.world +++ /dev/null @@ -1,12 +0,0 @@ -{ - "patterns": [ - { - "regexp": "map-x0*(\\\\dddd+)-y0*(\\\\dffd+)-.\\.tmx", - "multiplierX": 12000000, - "multiplierY": 48000000000000000000000000000000000000, - "offsetX": 240, - "offsetY": -240 - } - ], - "type": "world" -} \ No newline at end of file diff --git a/src/error.rs b/src/error.rs index 08f31bb0..434c2d62 100644 --- a/src/error.rs +++ b/src/error.rs @@ -57,8 +57,12 @@ pub enum Error { CsvDecodingError(CsvDecodingError), /// An error occurred when parsing an XML file, such as a TMX or TSX file. XmlDecodingError(xml::reader::Error), + #[cfg(feature = "world")] /// An error occurred when attempting to deserialize a JSON file. JsonDecodingError(serde_json::Error), + #[cfg(feature = "world")] + /// No regex captures were found. + CapturesNotFound, /// The XML stream ended before the document was fully parsed. PrematureEnd(String), /// The path given is invalid because it isn't contained in any folder. @@ -122,7 +126,10 @@ impl fmt::Display for Error { Error::Base64DecodingError(e) => write!(fmt, "{}", e), Error::CsvDecodingError(e) => write!(fmt, "{}", e), Error::XmlDecodingError(e) => write!(fmt, "{}", e), + #[cfg(feature = "world")] Error::JsonDecodingError(e) => write!(fmt, "{}", e), + #[cfg(feature = "world")] + Error::CapturesNotFound => write!(fmt, "No captures found in pattern"), Error::PrematureEnd(e) => write!(fmt, "{}", e), Error::PathIsNotFile => { write!( diff --git a/src/lib.rs b/src/lib.rs index 0a3b38d3..6905b50d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,6 +20,7 @@ mod template; mod tile; mod tileset; mod util; +#[cfg(feature = "world")] mod world; pub use animation::*; @@ -35,4 +36,5 @@ pub use reader::*; pub use template::*; pub use tile::*; pub use tileset::*; +#[cfg(feature = "world")] pub use world::*; diff --git a/src/loader.rs b/src/loader.rs index 8bb53e7b..b9e48be1 100644 --- a/src/loader.rs +++ b/src/loader.rs @@ -1,9 +1,12 @@ use std::path::Path; use crate::{ - DefaultResourceCache, FilesystemResourceReader, Map, ResourceCache, ResourceReader, Result, Tileset, World + DefaultResourceCache, FilesystemResourceReader, Map, ResourceCache, ResourceReader, Result, Tileset }; +#[cfg(feature = "world")] +use crate::World; + /// A type used for loading [`Map`]s and [`Tileset`]s. /// /// Internally, it holds a [`ResourceCache`] that, as its name implies, caches intermediate loading @@ -181,9 +184,11 @@ impl Loader { crate::parse::xml::parse_tileset(path.as_ref(), &mut self.reader, &mut self.cache) } - /// Parses a file hopefully containing a Tiled world and tries to parse it. + /// Parses a file hopefully containing a Tiled world and tries to parse it. All external files + /// will be loaded relative to the path given. + #[cfg(feature = "world")] pub fn load_world(&mut self, path: impl AsRef) -> Result { - crate::world::parse_world(path.as_ref(), &mut self.reader, &mut self.cache) + crate::world::parse_world(path.as_ref(), &mut self.reader) } /// Returns a reference to the loader's internal [`ResourceCache`]. diff --git a/src/world.rs b/src/world.rs index ea2c60a4..22d9d242 100644 --- a/src/world.rs +++ b/src/world.rs @@ -1,22 +1,24 @@ use std::{ - fs, io::Read, path::{Path, PathBuf} + io::Read, + path::{Path, PathBuf}, }; use regex::Regex; use serde::Deserialize; -use crate::{Error, Map, ResourceCache, ResourceReader}; +use crate::{Error, ResourceReader}; -/// A World is a collection of maps and their layout in the game world. +/// A World is a list of maps files or regex patterns that define a layout of TMX maps. +/// You can use the loader to further load the maps defined by the world. #[derive(Deserialize, PartialEq, Clone, Debug)] pub struct World { - /// The path first used in a ['ResourceReader'] to load this world. + /// The path first used in a [`ResourceReader`] to load this world. #[serde(skip_deserializing)] pub source: PathBuf, - /// The maps present in this world. + /// The [`WorldMap`]s defined in the world file. pub maps: Option>, /// Optional regex pattern to load maps. - patterns: Option>, + pub patterns: Option>, /// The type of world, which is arbitrary and set by the user. #[serde(rename = "type")] pub world_type: Option, @@ -40,7 +42,7 @@ pub struct WorldMap { /// A WorldPattern defines a regex pattern to automatically determine which maps to load and how to lay them out. #[derive(Deserialize, PartialEq, Clone, Debug)] -struct WorldPattern { +pub struct WorldPattern { /// The regex pattern to match against filenames. The first two capture groups should be the x integer and y integer positions. pub regexp: String, /// The multiplier for the x position. @@ -57,158 +59,92 @@ struct WorldPattern { pub offset_y: i32, } -/// Parse a Tiled World file from a path. -/// If a the Patterns field is present, it will attempt to build the maps list based on the regex patterns. -/// -/// ## Parameters -/// - `world_path`: The path to the world file. -/// -/// ## Example -/// ``` -/// # use tiled::Loader; -/// # -/// # fn main() { -/// # let mut loader = Loader::new(); -/// # let world = loader.load_world("assets/world/world_basic.world").unwrap(); -/// # -/// # for map in world.maps.unwrap() { -/// # println!("Map: {:?}", map); -/// # } -/// # } -/// ``` +impl WorldPattern { + /// Utility function to test a single path against the defined regexp field and returns a parsed WorldMap if it matches. + /// Returns none if the filename does not match the pattern. + pub fn capture_path(&self, path: &Path) -> Result { + let re = Regex::new(&self.regexp).unwrap(); + let captures = re + .captures(path.to_str().unwrap()) + .ok_or(Error::CapturesNotFound)?; + + let x = captures + .get(1) + .ok_or(Error::CapturesNotFound)? + .as_str() + .parse::() + .unwrap(); + let y = captures + .get(2) + .ok_or(Error::CapturesNotFound)? + .as_str() + .parse::() + .unwrap(); + + // Calculate x and y positions based on the multiplier and offset. + let x = x + .checked_mul(self.multiplier_x as i32) + .ok_or(Error::InvalidPropertyValue { + description: "multiplierX causes overflow".to_string(), + })? + .checked_add(self.offset_x) + .ok_or(Error::InvalidPropertyValue { + description: "offsetX causes overflow".to_string(), + })?; + + let y = y + .checked_mul(self.multiplier_y as i32) + .ok_or(Error::InvalidPropertyValue { + description: "multiplierY causes overflow".to_string(), + })? + .checked_add(self.offset_y) + .ok_or(Error::InvalidPropertyValue { + description: "offsetY causes overflow".to_string(), + })?; + + Ok(WorldMap { + filename: path.to_str().unwrap().to_owned(), + x, + y, + width: None, + height: None, + }) + } + + /// Utility function to test a list of paths against the defined regexp field. + /// Returns a parsed list of WorldMaps from any matched filenames. + pub fn capture_paths(&self, paths: Vec) -> Result, Error> { + paths + .iter() + .map(|path| self.capture_path(path.as_path())) + .collect::, _>>() + } +} + pub(crate) fn parse_world( world_path: &Path, reader: &mut impl ResourceReader, - cache: &mut impl ResourceCache, ) -> Result { - let mut path = reader.read_from(&world_path).map_err(|err| Error::ResourceLoadingError { - path: world_path.to_owned(), - err: Box::new(err), - })?; + let mut path = reader + .read_from(&world_path) + .map_err(|err| Error::ResourceLoadingError { + path: world_path.to_owned(), + err: Box::new(err), + })?; let mut world_string = String::new(); - path.read_to_string(&mut world_string).map_err(|err| Error::ResourceLoadingError { - path: world_path.to_owned(), - err: Box::new(err), - })?; + path.read_to_string(&mut world_string) + .map_err(|err| Error::ResourceLoadingError { + path: world_path.to_owned(), + err: Box::new(err), + })?; - let mut world: World = match serde_json::from_str(&world_string) { + let world: World = match serde_json::from_str(&world_string) { Ok(world) => world, Err(err) => { return Err(Error::JsonDecodingError(err)); } }; - if world.patterns.is_some() { - world.maps = match parse_world_pattern(world_path, &world.clone().patterns.unwrap()) { - Ok(maps) => Some(maps), - Err(err) => return Err(err), - }; - } - Ok(world) } - -/// If "patterns" key is present, it will attempt to build the maps list based on the regex patterns. -fn parse_world_pattern(path: &Path, patterns: &Vec) -> Result, Error> { - let mut maps = Vec::new(); - - let parent_dir = path.parent().ok_or(Error::ResourceLoadingError { - path: path.to_owned(), - err: Box::new(std::io::Error::from(std::io::ErrorKind::NotFound)), - })?; - - // There's no documentation on why "patterns" is a JSON array, so we'll just blast them into same maps list. - for pattern in patterns { - let files = fs::read_dir(parent_dir).map_err(|err| Error::ResourceLoadingError { - path: parent_dir.to_owned(), - err: Box::new(err), - })?; - - let re = Regex::new(&pattern.regexp).unwrap(); - let files = files - .filter_map(|entry| entry.ok()) - .filter(|entry| re.is_match(entry.path().file_name().unwrap().to_str().unwrap())) - .map(|entry| { - let filename = entry - .path() - .file_name() - .ok_or_else(|| Error::ResourceLoadingError { - path: path.to_owned(), - err: "Failed to get file name".into(), - })? - .to_str() - .ok_or_else(|| Error::ResourceLoadingError { - path: path.to_owned(), - err: "Failed to convert file name to string".into(), - })? - .to_owned(); - - let captures = re.captures(&filename).unwrap(); - - let x = captures - .get(1) - .ok_or_else(|| Error::ResourceLoadingError { - path: path.to_owned(), - err: format!("Failed to parse x pattern from file {}", filename).into(), - })? - .as_str() - .parse::() - .map_err(|e| Error::ResourceLoadingError { - path: path.to_owned(), - err: Box::new(e), - })?; - - let x = match x - .checked_mul(pattern.multiplier_x as i32) - .and_then(|x| x.checked_add(pattern.offset_x)) - { - Some(x) => x, - None => { - return Err(Error::ResourceLoadingError { - path: path.to_owned(), - err: "Arithmetic Overflow on multiplierX and offsetX".into(), - }) - } - }; - let y = captures - .get(2) - .ok_or_else(|| Error::ResourceLoadingError { - path: path.to_owned(), - err: format!("Failed to parse y pattern from file {}", filename).into(), - })? - .as_str() - .parse::() - .map_err(|e| Error::ResourceLoadingError { - path: path.to_owned(), - err: Box::new(e), - })?; - let y = match y - .checked_mul(pattern.multiplier_y as i32) - .and_then(|y| y.checked_add(pattern.offset_y)) - { - Some(y) => y, - None => { - return Err(Error::ResourceLoadingError { - path: path.to_owned(), - err: "Arithmetic Overflow on multiplierY and offsetY".into(), - }) - } - }; - Ok(WorldMap { - filename, - x, - y, - width: Some(pattern.multiplier_x), - height: Some(pattern.multiplier_y), - }) - }) - .collect::>(); - - for file in files { - maps.push(file?); - } - } - - Ok(maps) -} diff --git a/tests/lib.rs b/tests/lib.rs index e983c5a8..a3539b8a 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -68,6 +68,7 @@ fn test_external_tileset() { compare_everything_but_sources(&r, &e); } +#[cfg(feature = "world")] #[test] fn test_loading_world() { let mut loader = Loader::new(); @@ -85,27 +86,32 @@ fn test_loading_world() { assert_eq!(maps.len(), 2); } +#[cfg(feature = "world")] #[test] fn test_loading_world_pattern() { let mut loader = Loader::new(); let e = loader.load_world("assets/world/world_pattern.world").unwrap(); - let maps = e.maps.unwrap(); + assert_eq!(e.maps.is_none(), true); - assert_eq!(e.world_type.unwrap(), "world"); - assert_eq!(maps.len(), 2); -} + let patterns = e.patterns.unwrap(); -#[test] -fn test_bad_loading_world_pattern() { - let mut loader = Loader::new(); + assert_eq!(patterns.len(), 3); + + let map1 = patterns[0].capture_path(&PathBuf::from("assets/world/map-x04-y04-plains.tmx")).unwrap(); + assert_eq!(map1.filename, "assets/world/map-x04-y04-plains.tmx"); + assert_eq!(map1.x, 2800); + assert_eq!(map1.y, 1680); + + let map2 = patterns[1].capture_path(&PathBuf::from("overworld-x02-y02.tmx")).unwrap(); + assert_eq!(map2.filename, "overworld-x02-y02.tmx"); - let e = loader.load_world("assets/world/world_bad_pattern.world"); - assert!(e.is_err()); + let unmatched_map = patterns[0].capture_path(&PathBuf::from("bad_map.tmx")); + assert_eq!(unmatched_map.is_err(), true); - let e = loader.load_world("assets/world/world_pattern_bad.world"); - assert!(e.is_err()); + let overlow_map = patterns[2].capture_path(&PathBuf::from("map-x999-y999.tmx")); + assert_eq!(overlow_map.is_err(), true); } #[test] From 9695ab2f185061695ba72849c0a26dd750036926 Mon Sep 17 00:00:00 2001 From: JToTheThree Date: Wed, 18 Dec 2024 00:01:17 -0600 Subject: [PATCH 09/21] formatting and better docs --- src/loader.rs | 12 +++++++++--- tests/lib.rs | 12 +++++++++--- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/loader.rs b/src/loader.rs index b9e48be1..7a343cd2 100644 --- a/src/loader.rs +++ b/src/loader.rs @@ -1,7 +1,8 @@ use std::path::Path; use crate::{ - DefaultResourceCache, FilesystemResourceReader, Map, ResourceCache, ResourceReader, Result, Tileset + DefaultResourceCache, FilesystemResourceReader, Map, ResourceCache, ResourceReader, Result, + Tileset, }; #[cfg(feature = "world")] @@ -184,9 +185,14 @@ impl Loader { crate::parse::xml::parse_tileset(path.as_ref(), &mut self.reader, &mut self.cache) } - /// Parses a file hopefully containing a Tiled world and tries to parse it. All external files - /// will be loaded relative to the path given. #[cfg(feature = "world")] + /// Parses a file hopefully containing a Tiled world. + /// + /// The returned [`World`] provides the deserialized data from the world file. It does not load + /// any maps or tilesets. + /// ## Note + /// The ['WorldPattern`] struct provides [`WorldPattern::capture_path`] and [`WorldPattern::capture_paths`] + /// as utility functions to test paths and return parsed [`WorldMap`]s. pub fn load_world(&mut self, path: impl AsRef) -> Result { crate::world::parse_world(path.as_ref(), &mut self.reader) } diff --git a/tests/lib.rs b/tests/lib.rs index a3539b8a..f128eb20 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -91,7 +91,9 @@ fn test_loading_world() { fn test_loading_world_pattern() { let mut loader = Loader::new(); - let e = loader.load_world("assets/world/world_pattern.world").unwrap(); + let e = loader + .load_world("assets/world/world_pattern.world") + .unwrap(); assert_eq!(e.maps.is_none(), true); @@ -99,12 +101,16 @@ fn test_loading_world_pattern() { assert_eq!(patterns.len(), 3); - let map1 = patterns[0].capture_path(&PathBuf::from("assets/world/map-x04-y04-plains.tmx")).unwrap(); + let map1 = patterns[0] + .capture_path(&PathBuf::from("assets/world/map-x04-y04-plains.tmx")) + .unwrap(); assert_eq!(map1.filename, "assets/world/map-x04-y04-plains.tmx"); assert_eq!(map1.x, 2800); assert_eq!(map1.y, 1680); - let map2 = patterns[1].capture_path(&PathBuf::from("overworld-x02-y02.tmx")).unwrap(); + let map2 = patterns[1] + .capture_path(&PathBuf::from("overworld-x02-y02.tmx")) + .unwrap(); assert_eq!(map2.filename, "overworld-x02-y02.tmx"); let unmatched_map = patterns[0].capture_path(&PathBuf::from("bad_map.tmx")); From c87b118d3faf07dd6055af5b5e5a7c13a0315e6c Mon Sep 17 00:00:00 2001 From: JToTheThree Date: Wed, 18 Dec 2024 00:03:20 -0600 Subject: [PATCH 10/21] formatting and better docs --- src/loader.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/loader.rs b/src/loader.rs index 7a343cd2..9aa21667 100644 --- a/src/loader.rs +++ b/src/loader.rs @@ -191,7 +191,7 @@ impl Loader { /// The returned [`World`] provides the deserialized data from the world file. It does not load /// any maps or tilesets. /// ## Note - /// The ['WorldPattern`] struct provides [`WorldPattern::capture_path`] and [`WorldPattern::capture_paths`] + /// The ['WorldPattern`] struct provides [`WorldPattern::capture_path`] and [`WorldPattern::capture_paths`] /// as utility functions to test paths and return parsed [`WorldMap`]s. pub fn load_world(&mut self, path: impl AsRef) -> Result { crate::world::parse_world(path.as_ref(), &mut self.reader) From 3c56e9de3299886687643955ed5bb5d13d55bbdd Mon Sep 17 00:00:00 2001 From: JToTheThree Date: Wed, 18 Dec 2024 18:16:34 -0600 Subject: [PATCH 11/21] Moved pattern utils to World impl --- Cargo.toml | 1 + src/error.rs | 14 ++++- src/world.rs | 170 +++++++++++++++++++++++++++------------------------ tests/lib.rs | 32 +++++----- 4 files changed, 120 insertions(+), 97 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index eeb00cb0..45afde8c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,6 +39,7 @@ zstd = { version = "0.13.1", optional = true, default-features = false } flate2 = "1.0.28" serde = { version = "1.0.216", optional = true } serde_json = { version = "1.0.133", optional = true } +serde_regex = { version = "1.1.0", optional = true } regex = { version = "1.11.1", optional = true } [dev-dependencies.sfml] diff --git a/src/error.rs b/src/error.rs index 434c2d62..ed82d573 100644 --- a/src/error.rs +++ b/src/error.rs @@ -61,8 +61,13 @@ pub enum Error { /// An error occurred when attempting to deserialize a JSON file. JsonDecodingError(serde_json::Error), #[cfg(feature = "world")] - /// No regex captures were found. - CapturesNotFound, + /// Filename does not match any pattern in the world file. + NoMatchFound { + /// The filename that was not matched. + filename: String, + }, + /// A parameter is out of range or results in arithmetic underflow or overflow. + RangeError(String), /// The XML stream ended before the document was fully parsed. PrematureEnd(String), /// The path given is invalid because it isn't contained in any folder. @@ -129,7 +134,10 @@ impl fmt::Display for Error { #[cfg(feature = "world")] Error::JsonDecodingError(e) => write!(fmt, "{}", e), #[cfg(feature = "world")] - Error::CapturesNotFound => write!(fmt, "No captures found in pattern"), + Error::NoMatchFound { filename } => { + write!(fmt, "No match found for filename: '{}'", filename) + } + Error::RangeError(e) => write!(fmt, "Range error: {}", e), Error::PrematureEnd(e) => write!(fmt, "{}", e), Error::PathIsNotFile => { write!( diff --git a/src/world.rs b/src/world.rs index 22d9d242..43e3bf1a 100644 --- a/src/world.rs +++ b/src/world.rs @@ -15,13 +15,80 @@ pub struct World { /// The path first used in a [`ResourceReader`] to load this world. #[serde(skip_deserializing)] pub source: PathBuf, - /// The [`WorldMap`]s defined in the world file. + /// The [`WorldMap`]s defined by the world file. pub maps: Option>, /// Optional regex pattern to load maps. pub patterns: Option>, - /// The type of world, which is arbitrary and set by the user. - #[serde(rename = "type")] - pub world_type: Option, +} + +impl World { + /// Utility function to test a single filename against all defined patterns. + /// Returns a parsed [`WorldMap`] on the first matched pattern or an error if no patterns match. + pub fn match_filename(&self, filename: &str) -> Result { + // Tiled only tests tmx files that exist in the same directory as the world file. + // Supporting a proper path would misalign the crate with how tiled handles patterns. + if let Some(patterns) = &self.patterns { + for pattern in patterns { + let captures = match pattern.regexp.captures(filename) { + Some(captures) => captures, + None => continue, + }; + + let x = match captures.get(1) { + Some(x) => x.as_str().parse::().unwrap(), + None => continue, + }; + + let y = match captures.get(2) { + Some(y) => y.as_str().parse::().unwrap(), + None => continue, + }; + + // Calculate x and y positions based on the multiplier and offset. + let x = x + .checked_mul(pattern.multiplier_x) + .ok_or(Error::RangeError( + "Capture x * multiplierX causes overflow".to_string(), + ))? + .checked_add(pattern.offset_x) + .ok_or(Error::RangeError( + "Capture x * multiplierX + offsetX causes overflow".to_string(), + ))?; + + let y = y + .checked_mul(pattern.multiplier_y) + .ok_or(Error::RangeError( + "Capture y * multiplierY causes overflow".to_string(), + ))? + .checked_add(pattern.offset_y) + .ok_or(Error::RangeError( + "Capture y * multiplierY + offsetY causes overflow".to_string(), + ))?; + + // Returning the first matched pattern aligns with how Tiled handles patterns. + return Ok(WorldMap { + filename: filename.to_owned(), + x, + y, + width: None, + height: None, + }); + } + } + + Err(Error::NoMatchFound { + filename: filename.to_string(), + }) + } + + /// Utility function to test a vec of filenames against all defined patterns. + /// Returns a vec of results with the parsed [`WorldMap`]s if it matches the pattern. + pub fn match_filenames(&self, filenames: &Vec<&str>) -> Vec> { + filenames + .into_iter() + .map(|filename| self.match_filename(filename)) + .collect() + } } /// A WorldMap provides the information for a map in the world and its layout. @@ -35,89 +102,36 @@ pub struct WorldMap { /// The y position of the map. pub y: i32, /// The optional width of the map. - pub width: Option, + pub width: Option, /// The optional height of the map. - pub height: Option, + pub height: Option, } /// A WorldPattern defines a regex pattern to automatically determine which maps to load and how to lay them out. -#[derive(Deserialize, PartialEq, Clone, Debug)] +#[derive(Deserialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] pub struct WorldPattern { - /// The regex pattern to match against filenames. The first two capture groups should be the x integer and y integer positions. - pub regexp: String, + /// The regex pattern to match against filenames. + /// The first two capture groups should be the x integer and y integer positions. + #[serde(with = "serde_regex")] + pub regexp: Regex, /// The multiplier for the x position. - #[serde(rename = "multiplierX")] - pub multiplier_x: u32, + pub multiplier_x: i32, /// The multiplier for the y position. - #[serde(rename = "multiplierY")] - pub multiplier_y: u32, + pub multiplier_y: i32, /// The offset for the x position. - #[serde(rename = "offsetX")] pub offset_x: i32, /// The offset for the y position. - #[serde(rename = "offsetY")] pub offset_y: i32, } -impl WorldPattern { - /// Utility function to test a single path against the defined regexp field and returns a parsed WorldMap if it matches. - /// Returns none if the filename does not match the pattern. - pub fn capture_path(&self, path: &Path) -> Result { - let re = Regex::new(&self.regexp).unwrap(); - let captures = re - .captures(path.to_str().unwrap()) - .ok_or(Error::CapturesNotFound)?; - - let x = captures - .get(1) - .ok_or(Error::CapturesNotFound)? - .as_str() - .parse::() - .unwrap(); - let y = captures - .get(2) - .ok_or(Error::CapturesNotFound)? - .as_str() - .parse::() - .unwrap(); - - // Calculate x and y positions based on the multiplier and offset. - let x = x - .checked_mul(self.multiplier_x as i32) - .ok_or(Error::InvalidPropertyValue { - description: "multiplierX causes overflow".to_string(), - })? - .checked_add(self.offset_x) - .ok_or(Error::InvalidPropertyValue { - description: "offsetX causes overflow".to_string(), - })?; - - let y = y - .checked_mul(self.multiplier_y as i32) - .ok_or(Error::InvalidPropertyValue { - description: "multiplierY causes overflow".to_string(), - })? - .checked_add(self.offset_y) - .ok_or(Error::InvalidPropertyValue { - description: "offsetY causes overflow".to_string(), - })?; - - Ok(WorldMap { - filename: path.to_str().unwrap().to_owned(), - x, - y, - width: None, - height: None, - }) - } - - /// Utility function to test a list of paths against the defined regexp field. - /// Returns a parsed list of WorldMaps from any matched filenames. - pub fn capture_paths(&self, paths: Vec) -> Result, Error> { - paths - .iter() - .map(|path| self.capture_path(path.as_path())) - .collect::, _>>() +impl PartialEq for WorldPattern { + fn eq(&self, other: &Self) -> bool { + self.multiplier_x == other.multiplier_x + && self.multiplier_y == other.multiplier_y + && self.offset_x == other.offset_x + && self.offset_y == other.offset_y + && self.regexp.to_string() == other.regexp.to_string() } } @@ -139,12 +153,8 @@ pub(crate) fn parse_world( err: Box::new(err), })?; - let world: World = match serde_json::from_str(&world_string) { - Ok(world) => world, - Err(err) => { - return Err(Error::JsonDecodingError(err)); - } - }; + let world: World = + serde_json::from_str(&world_string).map_err(|err| Error::JsonDecodingError(err))?; Ok(world) } diff --git a/tests/lib.rs b/tests/lib.rs index f128eb20..f93ffdde 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -77,7 +77,6 @@ fn test_loading_world() { let maps = e.maps.unwrap(); - assert_eq!(e.world_type.unwrap(), "world"); assert_eq!(maps[0].filename, "map01.tmx"); assert_eq!(maps[1].x, 960); assert_eq!(maps[1].y, 0); @@ -97,27 +96,32 @@ fn test_loading_world_pattern() { assert_eq!(e.maps.is_none(), true); - let patterns = e.patterns.unwrap(); - + let patterns = e.patterns.as_ref().unwrap(); assert_eq!(patterns.len(), 3); - let map1 = patterns[0] - .capture_path(&PathBuf::from("assets/world/map-x04-y04-plains.tmx")) - .unwrap(); - assert_eq!(map1.filename, "assets/world/map-x04-y04-plains.tmx"); + let map1 = e.match_filename("map-x04-y04-plains.tmx").unwrap(); + + assert_eq!(map1.filename, "map-x04-y04-plains.tmx"); assert_eq!(map1.x, 2800); assert_eq!(map1.y, 1680); - let map2 = patterns[1] - .capture_path(&PathBuf::from("overworld-x02-y02.tmx")) - .unwrap(); + let map2 = e.match_filename("overworld-x02-y02.tmx").unwrap(); + assert_eq!(map2.filename, "overworld-x02-y02.tmx"); - let unmatched_map = patterns[0].capture_path(&PathBuf::from("bad_map.tmx")); - assert_eq!(unmatched_map.is_err(), true); + let filenames = vec!["bad_map.tmx", "OVERFLOW-x099-y099.tmx"]; + + let errors = vec![ + "No match found for filename: 'bad_map.tmx'", + "Range error: Capture x * multiplierX causes overflow", + ]; - let overlow_map = patterns[2].capture_path(&PathBuf::from("map-x999-y999.tmx")); - assert_eq!(overlow_map.is_err(), true); + let matches = e.match_filenames(&filenames); + + for (index, result) in matches.iter().enumerate() { + assert_eq!(result.is_err(), true); + assert_eq!(result.as_ref().err().unwrap().to_string(), errors[index]); + } } #[test] From 9eab95d5b8920d8bce39a247c90cea19f1325502 Mon Sep 17 00:00:00 2001 From: JToTheThree Date: Wed, 18 Dec 2024 21:00:37 -0600 Subject: [PATCH 12/21] Missed dep for world feature and cargo build step --- .github/workflows/rust.yml | 4 ++-- Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 109da256..d31ea725 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -27,10 +27,10 @@ jobs: run: sudo apt-get install -y libsfml-dev libcsfml-dev libasound2-dev libudev-dev - name: Build library - run: cargo build --lib --verbose + run: cargo build --lib --all-features --verbose - name: Run tests - run: cargo test --verbose --all-features + run: cargo test --all-features --verbose rustfmt: runs-on: ubuntu-24.04 diff --git a/Cargo.toml b/Cargo.toml index 45afde8c..dabf5761 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ include = ["src/**/*.rs", "README.md", "LICENSE", "CHANGELOG.md"] [features] default = ["zstd"] wasm = ["zstd/wasm"] -world = ["serde", "serde_json", "regex"] +world = ["regex", "serde", "serde_json", "serde_regex"] [lib] name = "tiled" From b36eac98799e552b5b46487acd4d37d6b90ed910 Mon Sep 17 00:00:00 2001 From: JToTheThree Date: Thu, 19 Dec 2024 09:46:26 -0600 Subject: [PATCH 13/21] match_path and match_paths --- src/error.rs | 6 +++--- src/world.rs | 18 ++++++++---------- tests/lib.rs | 16 ++++++++++------ 3 files changed, 21 insertions(+), 19 deletions(-) diff --git a/src/error.rs b/src/error.rs index ed82d573..2d5d4b56 100644 --- a/src/error.rs +++ b/src/error.rs @@ -64,7 +64,7 @@ pub enum Error { /// Filename does not match any pattern in the world file. NoMatchFound { /// The filename that was not matched. - filename: String, + path: PathBuf, }, /// A parameter is out of range or results in arithmetic underflow or overflow. RangeError(String), @@ -134,8 +134,8 @@ impl fmt::Display for Error { #[cfg(feature = "world")] Error::JsonDecodingError(e) => write!(fmt, "{}", e), #[cfg(feature = "world")] - Error::NoMatchFound { filename } => { - write!(fmt, "No match found for filename: '{}'", filename) + Error::NoMatchFound { path } => { + write!(fmt, "No match found for path: '{}'", path.to_string_lossy()) } Error::RangeError(e) => write!(fmt, "Range error: {}", e), Error::PrematureEnd(e) => write!(fmt, "{}", e), diff --git a/src/world.rs b/src/world.rs index 43e3bf1a..962624cd 100644 --- a/src/world.rs +++ b/src/world.rs @@ -22,14 +22,12 @@ pub struct World { } impl World { - /// Utility function to test a single filename against all defined patterns. + /// Utility function to test a single path against all defined patterns. /// Returns a parsed [`WorldMap`] on the first matched pattern or an error if no patterns match. - pub fn match_filename(&self, filename: &str) -> Result { - // Tiled only tests tmx files that exist in the same directory as the world file. - // Supporting a proper path would misalign the crate with how tiled handles patterns. + pub fn match_path(&self, path: impl AsRef) -> Result { if let Some(patterns) = &self.patterns { for pattern in patterns { - let captures = match pattern.regexp.captures(filename) { + let captures = match pattern.regexp.captures(path.as_ref().to_str().unwrap()) { Some(captures) => captures, None => continue, }; @@ -67,7 +65,7 @@ impl World { // Returning the first matched pattern aligns with how Tiled handles patterns. return Ok(WorldMap { - filename: filename.to_owned(), + filename: path.as_ref().to_str().unwrap().to_string(), x, y, width: None, @@ -77,16 +75,16 @@ impl World { } Err(Error::NoMatchFound { - filename: filename.to_string(), + path: path.as_ref().to_owned(), }) } /// Utility function to test a vec of filenames against all defined patterns. /// Returns a vec of results with the parsed [`WorldMap`]s if it matches the pattern. - pub fn match_filenames(&self, filenames: &Vec<&str>) -> Vec> { - filenames + pub fn match_paths>(&self, paths: &[P]) -> Vec> { + paths .into_iter() - .map(|filename| self.match_filename(filename)) + .map(|path| self.match_path(path)) .collect() } } diff --git a/tests/lib.rs b/tests/lib.rs index f93ffdde..4a6ff854 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -99,24 +99,28 @@ fn test_loading_world_pattern() { let patterns = e.patterns.as_ref().unwrap(); assert_eq!(patterns.len(), 3); - let map1 = e.match_filename("map-x04-y04-plains.tmx").unwrap(); + let map1 = e.match_path("map-x04-y04-plains.tmx").unwrap(); assert_eq!(map1.filename, "map-x04-y04-plains.tmx"); assert_eq!(map1.x, 2800); assert_eq!(map1.y, 1680); - let map2 = e.match_filename("overworld-x02-y02.tmx").unwrap(); + let map2 = e + .match_path(PathBuf::from("assets/overworld-x02-y02.tmx")) + .unwrap(); - assert_eq!(map2.filename, "overworld-x02-y02.tmx"); + assert_eq!(map2.filename, "assets/overworld-x02-y02.tmx"); + // Test to determine if we correctly hit the second pattern + assert_eq!(map2.x, 5472); - let filenames = vec!["bad_map.tmx", "OVERFLOW-x099-y099.tmx"]; + let paths = vec!["bad_map.tmx", "OVERFLOW-x099-y099.tmx"]; let errors = vec![ - "No match found for filename: 'bad_map.tmx'", + "No match found for path: 'bad_map.tmx'", "Range error: Capture x * multiplierX causes overflow", ]; - let matches = e.match_filenames(&filenames); + let matches = e.match_paths(&paths); for (index, result) in matches.iter().enumerate() { assert_eq!(result.is_err(), true); From e72e805a4a6e1a7c23c925966fc7d6eaea575c28 Mon Sep 17 00:00:00 2001 From: JToTheThree Date: Sat, 21 Dec 2024 14:14:25 -0600 Subject: [PATCH 14/21] add match_path to WorldPattern --- src/world.rs | 112 +++++++++++++++++++++++++++++++-------------------- 1 file changed, 68 insertions(+), 44 deletions(-) diff --git a/src/world.rs b/src/world.rs index 962624cd..aae01c3c 100644 --- a/src/world.rs +++ b/src/world.rs @@ -27,50 +27,12 @@ impl World { pub fn match_path(&self, path: impl AsRef) -> Result { if let Some(patterns) = &self.patterns { for pattern in patterns { - let captures = match pattern.regexp.captures(path.as_ref().to_str().unwrap()) { - Some(captures) => captures, - None => continue, - }; - - let x = match captures.get(1) { - Some(x) => x.as_str().parse::().unwrap(), - None => continue, - }; - - let y = match captures.get(2) { - Some(y) => y.as_str().parse::().unwrap(), - None => continue, - }; - - // Calculate x and y positions based on the multiplier and offset. - let x = x - .checked_mul(pattern.multiplier_x) - .ok_or(Error::RangeError( - "Capture x * multiplierX causes overflow".to_string(), - ))? - .checked_add(pattern.offset_x) - .ok_or(Error::RangeError( - "Capture x * multiplierX + offsetX causes overflow".to_string(), - ))?; - - let y = y - .checked_mul(pattern.multiplier_y) - .ok_or(Error::RangeError( - "Capture y * multiplierY causes overflow".to_string(), - ))? - .checked_add(pattern.offset_y) - .ok_or(Error::RangeError( - "Capture y * multiplierY + offsetY causes overflow".to_string(), - ))?; - - // Returning the first matched pattern aligns with how Tiled handles patterns. - return Ok(WorldMap { - filename: path.as_ref().to_str().unwrap().to_string(), - x, - y, - width: None, - height: None, - }); + match pattern.match_path(path.as_ref()) { + Ok(world_map) => return Ok(world_map), + // We ignore matches here as the path may be matched by another pattern. + Err(Error::NoMatchFound { .. }) => continue, + Err(err) => return Err(err), + } } } @@ -133,6 +95,68 @@ impl PartialEq for WorldPattern { } } +impl WorldPattern { + /// Utility function to test a path against this pattern. + /// Returns a parsed [`WorldMap`] on the first matched pattern or an error if no patterns match. + pub fn match_path(&self, path: impl AsRef) -> Result { + let captures = match self.regexp.captures(path.as_ref().to_str().unwrap()) { + Some(captures) => captures, + None => { + return Err(Error::NoMatchFound { + path: path.as_ref().to_owned(), + }) + } + }; + + let x = match captures.get(1) { + Some(x) => x.as_str().parse::().unwrap(), + None => { + return Err(Error::NoMatchFound { + path: path.as_ref().to_owned(), + }) + } + }; + + let y = match captures.get(2) { + Some(y) => y.as_str().parse::().unwrap(), + None => { + return Err(Error::NoMatchFound { + path: path.as_ref().to_owned(), + }) + } + }; + + // Calculate x and y positions based on the multiplier and offset. + let x = x + .checked_mul(self.multiplier_x) + .ok_or(Error::RangeError( + "Capture x * multiplierX causes overflow".to_string(), + ))? + .checked_add(self.offset_x) + .ok_or(Error::RangeError( + "Capture x * multiplierX + offsetX causes overflow".to_string(), + ))?; + + let y = y + .checked_mul(self.multiplier_y) + .ok_or(Error::RangeError( + "Capture y * multiplierY causes overflow".to_string(), + ))? + .checked_add(self.offset_y) + .ok_or(Error::RangeError( + "Capture y * multiplierY + offsetY causes overflow".to_string(), + ))?; + + Ok(WorldMap { + filename: path.as_ref().to_str().unwrap().to_string(), + x, + y, + width: None, + height: None, + }) + } +} + pub(crate) fn parse_world( world_path: &Path, reader: &mut impl ResourceReader, From 37f0ee6971e8eb31b2ee6aecc6198bef723b70dd Mon Sep 17 00:00:00 2001 From: JToTheThree Date: Sat, 21 Dec 2024 14:30:30 -0600 Subject: [PATCH 15/21] reduce utf-8 checks on path iteration --- src/world.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/world.rs b/src/world.rs index aae01c3c..2e21c3df 100644 --- a/src/world.rs +++ b/src/world.rs @@ -25,9 +25,11 @@ impl World { /// Utility function to test a single path against all defined patterns. /// Returns a parsed [`WorldMap`] on the first matched pattern or an error if no patterns match. pub fn match_path(&self, path: impl AsRef) -> Result { + let path_str = path.as_ref().to_str().expect("obtaining valid UTF-8 path"); + if let Some(patterns) = &self.patterns { for pattern in patterns { - match pattern.match_path(path.as_ref()) { + match pattern.match_path(path_str) { Ok(world_map) => return Ok(world_map), // We ignore matches here as the path may be matched by another pattern. Err(Error::NoMatchFound { .. }) => continue, @@ -99,7 +101,9 @@ impl WorldPattern { /// Utility function to test a path against this pattern. /// Returns a parsed [`WorldMap`] on the first matched pattern or an error if no patterns match. pub fn match_path(&self, path: impl AsRef) -> Result { - let captures = match self.regexp.captures(path.as_ref().to_str().unwrap()) { + let path_str = path.as_ref().to_str().expect("obtaining valid UTF-8 path"); + + let captures = match self.regexp.captures(path_str) { Some(captures) => captures, None => { return Err(Error::NoMatchFound { @@ -148,7 +152,7 @@ impl WorldPattern { ))?; Ok(WorldMap { - filename: path.as_ref().to_str().unwrap().to_string(), + filename: path_str.to_string(), x, y, width: None, From 534ab0a4083d7537785745647e63478f9c22de86 Mon Sep 17 00:00:00 2001 From: JToTheThree Date: Sun, 22 Dec 2024 13:32:16 -0600 Subject: [PATCH 16/21] match_path_impl and readme update --- README.md | 2 +- src/error.rs | 4 ++-- src/world.rs | 20 ++++++++++++-------- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 1f437fa2..4b4c445e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # rs-tiled ```toml -tiled = "0.13.0" +tiled = "0.14.0" ``` [![Rust](https://github.com/mapeditor/rs-tiled/actions/workflows/rust.yml/badge.svg)](https://github.com/mapeditor/rs-tiled/actions/workflows/rust.yml) diff --git a/src/error.rs b/src/error.rs index 2d5d4b56..ca6172e2 100644 --- a/src/error.rs +++ b/src/error.rs @@ -64,7 +64,7 @@ pub enum Error { /// Filename does not match any pattern in the world file. NoMatchFound { /// The filename that was not matched. - path: PathBuf, + path: String, }, /// A parameter is out of range or results in arithmetic underflow or overflow. RangeError(String), @@ -135,7 +135,7 @@ impl fmt::Display for Error { Error::JsonDecodingError(e) => write!(fmt, "{}", e), #[cfg(feature = "world")] Error::NoMatchFound { path } => { - write!(fmt, "No match found for path: '{}'", path.to_string_lossy()) + write!(fmt, "No match found for path: '{}'", path) } Error::RangeError(e) => write!(fmt, "Range error: {}", e), Error::PrematureEnd(e) => write!(fmt, "{}", e), diff --git a/src/world.rs b/src/world.rs index 2e21c3df..e7c88249 100644 --- a/src/world.rs +++ b/src/world.rs @@ -29,9 +29,9 @@ impl World { if let Some(patterns) = &self.patterns { for pattern in patterns { - match pattern.match_path(path_str) { + match pattern.match_path_impl(path_str) { Ok(world_map) => return Ok(world_map), - // We ignore matches here as the path may be matched by another pattern. + // We ignore match errors here as the path may be matched by another pattern. Err(Error::NoMatchFound { .. }) => continue, Err(err) => return Err(err), } @@ -39,7 +39,7 @@ impl World { } Err(Error::NoMatchFound { - path: path.as_ref().to_owned(), + path: path_str.to_owned(), }) } @@ -103,11 +103,15 @@ impl WorldPattern { pub fn match_path(&self, path: impl AsRef) -> Result { let path_str = path.as_ref().to_str().expect("obtaining valid UTF-8 path"); - let captures = match self.regexp.captures(path_str) { + self.match_path_impl(path_str) + } + + pub(crate) fn match_path_impl(&self, path: &str) -> Result { + let captures = match self.regexp.captures(path) { Some(captures) => captures, None => { return Err(Error::NoMatchFound { - path: path.as_ref().to_owned(), + path: path.to_owned(), }) } }; @@ -116,7 +120,7 @@ impl WorldPattern { Some(x) => x.as_str().parse::().unwrap(), None => { return Err(Error::NoMatchFound { - path: path.as_ref().to_owned(), + path: path.to_owned(), }) } }; @@ -125,7 +129,7 @@ impl WorldPattern { Some(y) => y.as_str().parse::().unwrap(), None => { return Err(Error::NoMatchFound { - path: path.as_ref().to_owned(), + path: path.to_owned(), }) } }; @@ -152,7 +156,7 @@ impl WorldPattern { ))?; Ok(WorldMap { - filename: path_str.to_string(), + filename: path.to_owned(), x, y, width: None, From 584b8930da5f616a19a70255593bc383189c26c6 Mon Sep 17 00:00:00 2001 From: John Date: Fri, 27 Dec 2024 17:58:11 -0600 Subject: [PATCH 17/21] empty vecs instead of option --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4b4c445e..eb543414 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ let mut loader = Loader::with_reader( // Doing this embedding is useful for places where the OS filesystem is not available (e.g. WASM applications). |path: &std::path::Path| -> std::io::Result<_> { if path == std::path::Path::new("/my-map.tmx") { - Ok(std::io::Cursor::new(include_bytes!("assets/tiled_csv.tmx"))) + Ok(std::io::Cursor::new(include_bytes!("../assets/tiled_csv.tmx"))) } else { Err(std::io::ErrorKind::NotFound.into()) } @@ -86,7 +86,7 @@ impl tiled::ResourceReader for MyReader { // really dumb example implementation that just keeps resources in memory fn read_from(&mut self, path: &std::path::Path) -> std::result::Result { if path == std::path::Path::new("my_map.tmx") { - Ok(Cursor::new(include_bytes!("assets/tiled_xml.tmx"))) + Ok(Cursor::new(include_bytes!("../assets/tiled_xml.tmx"))) } else { Err(std::io::Error::new(std::io::ErrorKind::NotFound, "file not found")) } From 7930290719d52fd9ac1b88e1c121565bd14ff00e Mon Sep 17 00:00:00 2001 From: John Date: Fri, 27 Dec 2024 17:58:20 -0600 Subject: [PATCH 18/21] empty vecs instead of option --- src/world.rs | 20 ++++++++++---------- tests/lib.rs | 23 ++++++++++------------- 2 files changed, 20 insertions(+), 23 deletions(-) diff --git a/src/world.rs b/src/world.rs index e7c88249..4be6d705 100644 --- a/src/world.rs +++ b/src/world.rs @@ -16,9 +16,11 @@ pub struct World { #[serde(skip_deserializing)] pub source: PathBuf, /// The [`WorldMap`]s defined by the world file. - pub maps: Option>, + #[serde(skip_serializing_if = "Vec::is_empty", default)] + pub maps: Vec, /// Optional regex pattern to load maps. - pub patterns: Option>, + #[serde(skip_serializing_if = "Vec::is_empty", default)] + pub patterns: Vec, } impl World { @@ -27,14 +29,12 @@ impl World { pub fn match_path(&self, path: impl AsRef) -> Result { let path_str = path.as_ref().to_str().expect("obtaining valid UTF-8 path"); - if let Some(patterns) = &self.patterns { - for pattern in patterns { - match pattern.match_path_impl(path_str) { - Ok(world_map) => return Ok(world_map), - // We ignore match errors here as the path may be matched by another pattern. - Err(Error::NoMatchFound { .. }) => continue, - Err(err) => return Err(err), - } + for pattern in self.patterns.iter() { + match pattern.match_path_impl(path_str) { + Ok(world_map) => return Ok(world_map), + // We ignore match errors here as the path may be matched by another pattern. + Err(Error::NoMatchFound { .. }) => continue, + Err(err) => return Err(err), } } diff --git a/tests/lib.rs b/tests/lib.rs index 4a6ff854..0779c850 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -70,34 +70,31 @@ fn test_external_tileset() { #[cfg(feature = "world")] #[test] -fn test_loading_world() { +fn test_world() { let mut loader = Loader::new(); let e = loader.load_world("assets/world/world_basic.world").unwrap(); - let maps = e.maps.unwrap(); - - assert_eq!(maps[0].filename, "map01.tmx"); - assert_eq!(maps[1].x, 960); - assert_eq!(maps[1].y, 0); - assert_eq!(maps[1].width, Some(960)); - assert_eq!(maps[1].height, Some(640)); - assert_eq!(maps.len(), 2); + assert_eq!(e.maps[0].filename, "map01.tmx"); + assert_eq!(e.maps[1].x, 960); + assert_eq!(e.maps[1].y, 0); + assert_eq!(e.maps[1].width, Some(960)); + assert_eq!(e.maps[1].height, Some(640)); + assert_eq!(e.maps.len(), 2); } #[cfg(feature = "world")] #[test] -fn test_loading_world_pattern() { +fn test_world_pattern() { let mut loader = Loader::new(); let e = loader .load_world("assets/world/world_pattern.world") .unwrap(); - assert_eq!(e.maps.is_none(), true); + assert_eq!(e.maps.len(), 0); - let patterns = e.patterns.as_ref().unwrap(); - assert_eq!(patterns.len(), 3); + assert_eq!(e.patterns.len(), 3); let map1 = e.match_path("map-x04-y04-plains.tmx").unwrap(); From 7f3b082c6a7dfa40944d465eeb67ad3c93cf0140 Mon Sep 17 00:00:00 2001 From: John Date: Fri, 27 Dec 2024 18:05:15 -0600 Subject: [PATCH 19/21] Oddities with readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index eb543414..4b4c445e 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ let mut loader = Loader::with_reader( // Doing this embedding is useful for places where the OS filesystem is not available (e.g. WASM applications). |path: &std::path::Path| -> std::io::Result<_> { if path == std::path::Path::new("/my-map.tmx") { - Ok(std::io::Cursor::new(include_bytes!("../assets/tiled_csv.tmx"))) + Ok(std::io::Cursor::new(include_bytes!("assets/tiled_csv.tmx"))) } else { Err(std::io::ErrorKind::NotFound.into()) } @@ -86,7 +86,7 @@ impl tiled::ResourceReader for MyReader { // really dumb example implementation that just keeps resources in memory fn read_from(&mut self, path: &std::path::Path) -> std::result::Result { if path == std::path::Path::new("my_map.tmx") { - Ok(Cursor::new(include_bytes!("../assets/tiled_xml.tmx"))) + Ok(Cursor::new(include_bytes!("assets/tiled_xml.tmx"))) } else { Err(std::io::Error::new(std::io::ErrorKind::NotFound, "file not found")) } From 82d9bfdc41f33d92f283b0f9c63c3912bbf01e13 Mon Sep 17 00:00:00 2001 From: John Date: Fri, 27 Dec 2024 18:11:44 -0600 Subject: [PATCH 20/21] fix load_world docs --- src/loader.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/loader.rs b/src/loader.rs index 9aa21667..fcf945eb 100644 --- a/src/loader.rs +++ b/src/loader.rs @@ -191,7 +191,7 @@ impl Loader { /// The returned [`World`] provides the deserialized data from the world file. It does not load /// any maps or tilesets. /// ## Note - /// The ['WorldPattern`] struct provides [`WorldPattern::capture_path`] and [`WorldPattern::capture_paths`] + /// The ['WorldPattern`] struct provides [`WorldPattern::match_path`] and [`WorldPattern::match_paths`] /// as utility functions to test paths and return parsed [`WorldMap`]s. pub fn load_world(&mut self, path: impl AsRef) -> Result { crate::world::parse_world(path.as_ref(), &mut self.reader) From 091d07439635dbd624b6d8b52aef285b958567c9 Mon Sep 17 00:00:00 2001 From: JToTheThree Date: Sat, 28 Dec 2024 10:15:08 -0600 Subject: [PATCH 21/21] fix source not populating --- src/world.rs | 4 +++- tests/lib.rs | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/world.rs b/src/world.rs index 4be6d705..faf663a5 100644 --- a/src/world.rs +++ b/src/world.rs @@ -183,8 +183,10 @@ pub(crate) fn parse_world( err: Box::new(err), })?; - let world: World = + let mut world: World = serde_json::from_str(&world_string).map_err(|err| Error::JsonDecodingError(err))?; + world.source = world_path.to_owned(); + Ok(world) } diff --git a/tests/lib.rs b/tests/lib.rs index 0779c850..4a4dbcec 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -92,8 +92,8 @@ fn test_world_pattern() { .load_world("assets/world/world_pattern.world") .unwrap(); + assert_eq!(e.source, PathBuf::from("assets/world/world_pattern.world")); assert_eq!(e.maps.len(), 0); - assert_eq!(e.patterns.len(), 3); let map1 = e.match_path("map-x04-y04-plains.tmx").unwrap();