diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000000000000000000000000000000000..16b486a88261c6699751c5c44fc6283019d8faa3 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,14 @@ +# Set the default behavior, in case people don't have core.autocrlf set. +* text=auto + +# Explicitly declare text files we want to always be normalized and converted +# to native line endings on checkout. +*.rs text + +# Normalize example TMX & TSX file line endings for consistent tests among all platforms. +*.tmx text eol=crlf +*.tsx text eol=crlf + +# Denote all files that are truly binary and should not be modified. +*.png binary +*.jpg binary diff --git a/CHANGELOG.md b/CHANGELOG.md index d3b9a6498a2e8a271527f30647b11b77fcb13f6c..893e365d6bf441460b0723b6d485cbb5ebcd5437 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,22 +6,53 @@ 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). ## [Unreleased] +### Changed +- Reorganized crate: + - `parse_file`, `parse` -> `Map::parse_file` with optional path. + - `parse_with_path` -> `Map::parse_reader` + - `parse_tileset` -> `Tileset::parse` + - `Frame` has been moved to the `animation` module. + - `ParseTileError` & `TiledError` have been moved into the `error` module. + - `Image` has been moved into the `image` module. + - `LayerTile`, `Layer`, `LayerData`, `ImageLayer` & `Chunk` have been moved into the `layers` module. + - `Map` & `Orientation` have been moved into the `map` module. + - `ObjectGroup`, `ObjectShape` & `Object` have been moved into the `objects` module. + - `Colour`, `PropertyValue` & `Properties` have been moved into the `properties` module. + - All mentions of `Colour` have been changed to `Color` for consistency with the Tiled dataformat. + - `Tile` has been moved into the `tile` module. + - `Tileset` has been moved into the `tileset` module. + - `Map::get_tileset_by_gid` -> `Map::tileset_by_gid` +- Tile now has `image` instead of `images`. ([Issue comment](https://github.com/mapeditor/rs-tiled/issues/103#issuecomment-940773123)) +- Tileset now has `image` instead of `images`. +- `Image::source` is now a `PathBuf` instead of a `String`. +- Functions that took in `&Path` now take `impl AsRef<Path>`. +- Refactored internals. +- Fixed library warnings. +- Bumped `zstd` to `0.9`. +- Fixed markdown formatting in the `CONTRIBUTORS` file. ### Added - +- `Tileset::source` for obtaining where the tileset actually came from. +- `Tileset::columns`. +- `layers::Layer::id`. - Support for 'object'-type properties. +- Documentation for map members. +- Tests for `tiled_base64_zstandard.tmx`. +- `.gitattributes` for line ending consistency. +- Support for multiline string properties. +- MIT license file. -## [0.9.2] - 2020-Apr-25 -### Added +## [0.9.5] +TODO -- Properties to Tilesets. -- Test verifying `tileset.properties` +## [0.9.4] +TODO -### Changed +## [0.9.3] +TODO -- - -### Removed - -- +## [0.9.2] - 2020-Apr-25 +### Added +- Properties to Tilesets. +- Test verifying `tileset.properties` \ No newline at end of file diff --git a/README.md b/README.md index f92ec5fa6010bc2286657c3e9223734bb520cedb..d5c3bbaa1f6d65df128333405752d6d30c0085d4 100644 --- a/README.md +++ b/README.md @@ -18,22 +18,12 @@ to the dependencies section of your Cargo.toml. ### Example ```rust -extern crate serialize; -extern crate tiled; - -use std::fs::File; -use std::io::BufReader; -use std::path::Path; - -use tiled::parse; +use tiled::map::Map; fn main() { - let file = File::open(&Path::new("assets/tiled_base64_zlib.tmx")).unwrap(); - println!("Opened file"); - let reader = BufReader::new(file); - let map = parse(reader).unwrap(); + let map = Map::parse_file("assets/tiled_base64_zlib.tmx").unwrap(); println!("{:?}", map); - println!("{:?}", map.get_tileset_by_gid(22)); + println!("{:?}", map.tileset_by_gid(22)); } ``` diff --git a/examples/main.rs b/examples/main.rs index c4ae445d7c21f05a1d29504181b649ad76678660..afdcb6d209980f26edeec11c2e21b7537d822815 100644 --- a/examples/main.rs +++ b/examples/main.rs @@ -1,11 +1,7 @@ -use std::fs::File; -use std::path::Path; -use tiled::parse; +use tiled::map::Map; fn main() { - let file = File::open(&Path::new("assets/tiled_base64_zlib.tmx")).unwrap(); - println!("Opened file"); - let map = parse(file).unwrap(); + let map = Map::parse_file("assets/tiled_base64_zlib.tmx").unwrap(); println!("{:?}", map); - println!("{:?}", map.get_tileset_by_gid(22)); + println!("{:?}", map.tileset_by_gid(22)); } diff --git a/src/error.rs b/src/error.rs index 6a502c21c01edaa186ad2a9cca205ee7c963e74f..55c4d032fe695152bc40b9017fdb9a8db4e4b1d4 100644 --- a/src/error.rs +++ b/src/error.rs @@ -2,7 +2,7 @@ use std::fmt; #[derive(Debug, Copy, Clone)] pub enum ParseTileError { - ColourError, + ColorError, OrientationError, } @@ -18,6 +18,11 @@ pub enum TiledError { Base64DecodingError(base64::DecodeError), XmlDecodingError(xml::reader::Error), PrematureEnd(String), + /// Tried to parse external data of an object without a file location, + /// e.g. by using Map::parse_reader. + SourceRequired { + object_to_parse: String, + }, Other(String), } @@ -29,6 +34,11 @@ impl fmt::Display for TiledError { TiledError::Base64DecodingError(ref e) => write!(fmt, "{}", e), TiledError::XmlDecodingError(ref e) => write!(fmt, "{}", e), TiledError::PrematureEnd(ref e) => write!(fmt, "{}", e), + TiledError::SourceRequired { + ref object_to_parse, + } => { + write!(fmt, "Tried to parse external {} without a file location, e.g. by using Map::parse_reader.", object_to_parse) + } TiledError::Other(ref s) => write!(fmt, "{}", s), } } @@ -43,6 +53,7 @@ impl std::error::Error for TiledError { TiledError::Base64DecodingError(ref e) => Some(e as &dyn std::error::Error), TiledError::XmlDecodingError(ref e) => Some(e as &dyn std::error::Error), TiledError::PrematureEnd(_) => None, + TiledError::SourceRequired { .. } => None, TiledError::Other(_) => None, } } diff --git a/src/image.rs b/src/image.rs index 351cc5bf61ebf59f25543a9ed454780bb4fecc0b..a7cf8eff2874bc13208101e6e996c93ab313e614 100644 --- a/src/image.rs +++ b/src/image.rs @@ -1,22 +1,33 @@ -use std::io::Read; +use std::{ + io::Read, + path::{Path, PathBuf}, +}; use xml::{attribute::OwnedAttribute, EventReader}; -use crate::{error::TiledError, properties::Colour, util::*}; +use crate::{error::TiledError, properties::Color, util::*}; #[derive(Debug, PartialEq, Eq, Clone)] pub struct Image { - /// The filepath of the image - pub source: String, + /// The filepath of the image. + /// + /// ## Note + /// The crate does not currently support embedded images (Even though Tiled + /// does not allow creating maps with embedded image data, the TMX format does; [source]) + /// + /// [source]: https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#image + // TODO: Embedded images + pub source: PathBuf, pub width: i32, pub height: i32, - pub transparent_colour: Option<Colour>, + pub transparent_colour: Option<Color>, } impl Image { pub(crate) fn new<R: Read>( parser: &mut EventReader<R>, attrs: Vec<OwnedAttribute>, + path_relative_to: impl AsRef<Path>, ) -> Result<Image, TiledError> { let (c, (s, w, h)) = get_attrs!( attrs, @@ -28,12 +39,12 @@ impl Image { ("width", width, |v:String| v.parse().ok()), ("height", height, |v:String| v.parse().ok()), ], - TiledError::MalformedAttributes("image must have a source, width and height with correct types".to_string()) + TiledError::MalformedAttributes("Image must have a source, width and height with correct types".to_string()) ); parse_tag!(parser, "image", { "" => |_| Ok(()) }); Ok(Image { - source: s, + source: path_relative_to.as_ref().join(s), width: w, height: h, transparent_colour: c, diff --git a/src/layers.rs b/src/layers.rs index 4d65d759e24e1c5f6a36da6278f6e0354edb5238..883426a03d4d2c47176f0d420c3982a022f1422b 100644 --- a/src/layers.rs +++ b/src/layers.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, io::Read}; +use std::{collections::HashMap, io::Read, path::Path}; use xml::{attribute::OwnedAttribute, EventReader}; @@ -86,7 +86,7 @@ impl Layer { parse_tag!(parser, "layer", { "data" => |attrs| { if infinite { - tiles = parse_infinite_data(parser, attrs, width)?; + tiles = parse_infinite_data(parser, attrs)?; } else { tiles = parse_data(parser, attrs, width)?; } @@ -137,6 +137,7 @@ impl ImageLayer { parser: &mut EventReader<R>, attrs: Vec<OwnedAttribute>, layer_index: u32, + path_relative_to: Option<&Path>, ) -> Result<ImageLayer, TiledError> { let ((o, v, ox, oy, n, id), ()) = get_attrs!( attrs, @@ -156,7 +157,7 @@ impl ImageLayer { let mut image: Option<Image> = None; parse_tag!(parser, "imagelayer", { "image" => |attrs| { - image = Some(Image::new(parser, attrs)?); + image = Some(Image::new(parser, attrs, path_relative_to.ok_or(TiledError::SourceRequired{object_to_parse: "Image".to_string()})?)?); Ok(()) }, "properties" => |_| { diff --git a/src/lib.rs b/src/lib.rs index 06805e70b8daa4ce9e7102a1f65b57bc57d54e91..2033ef1188ba07f1a3501cfb2d3f1f5213cb5548 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,55 +8,3 @@ pub mod properties; pub mod tile; pub mod tileset; mod util; - -use base64; - -use error::*; -use image::*; -use layers::*; -use map::*; -use std::collections::HashMap; -use std::fmt; -use std::fs::File; -use std::io::Read; -use std::path::{Path, PathBuf}; -use std::str::FromStr; -use tile::*; -use tileset::*; -use util::*; -use xml::attribute::OwnedAttribute; -use xml::reader::XmlEvent; -use xml::reader::{Error as XmlError, EventReader}; - -// TODO move these -/// Parse a buffer hopefully containing the contents of a Tiled file and try to -/// parse it. This augments `parse` with a file location: some engines -/// (e.g. Amethyst) simply hand over a byte stream (and file location) for parsing, -/// in which case this function may be required. -pub fn parse_with_path<R: Read>(reader: R, path: &Path) -> Result<Map, TiledError> { - parse_impl(reader, Some(path)) -} - -/// Parse a file hopefully containing a Tiled map and try to parse it. If the -/// file has an external tileset, the tileset file will be loaded using a path -/// relative to the map file's path. -pub fn parse_file(path: &Path) -> Result<Map, TiledError> { - let file = File::open(path) - .map_err(|_| TiledError::Other(format!("Map file not found: {:?}", path)))?; - parse_impl(file, Some(path)) -} - -/// Parse a buffer hopefully containing the contents of a Tiled file and try to -/// parse it. -pub fn parse<R: Read>(reader: R) -> Result<Map, TiledError> { - parse_impl(reader, None) -} - -/// Parse a buffer hopefully containing the contents of a Tiled tileset. -/// -/// External tilesets do not have a firstgid attribute. That lives in the -/// map. You must pass in `first_gid`. If you do not need to use gids for anything, -/// passing in 1 will work fine. -pub fn parse_tileset<R: Read>(reader: R, first_gid: u32) -> Result<Tileset, TiledError> { - Tileset::new_external(reader, first_gid) -} diff --git a/src/map.rs b/src/map.rs index f9271fa2dd1234210ab2dcb27662a1b17190750f..e22ff65f977b797ed717b5095d287249e3ff5110 100644 --- a/src/map.rs +++ b/src/map.rs @@ -1,38 +1,82 @@ -use std::{collections::HashMap, fmt, io::Read, path::Path, str::FromStr}; +use std::{collections::HashMap, fmt, fs::File, io::Read, path::Path, str::FromStr}; -use xml::{attribute::OwnedAttribute, EventReader}; +use xml::{attribute::OwnedAttribute, reader::XmlEvent, EventReader}; use crate::{ error::{ParseTileError, TiledError}, layers::{ImageLayer, Layer}, objects::ObjectGroup, - properties::{parse_properties, Colour, Properties}, + properties::{parse_properties, Color, Properties}, tileset::Tileset, - util::*, + util::{get_attrs, parse_tag}, }; -/// All Tiled files will be parsed into this. Holds all the layers and tilesets +/// All Tiled map files will be parsed into this. Holds all the layers and tilesets. #[derive(Debug, PartialEq, Clone)] pub struct Map { + /// The TMX format version this map was saved to. pub version: String, pub orientation: Orientation, - /// Width of the map, in tiles + /// Width of the map, in tiles. pub width: u32, - /// Height of the map, in tiles + /// Height of the map, in tiles. pub height: u32, + /// Tile width, in pixels. pub tile_width: u32, + /// Tile height, in pixels. pub tile_height: u32, + /// The tilesets present in this map. pub tilesets: Vec<Tileset>, + /// The tile layers present in this map. pub layers: Vec<Layer>, pub image_layers: Vec<ImageLayer>, pub object_groups: Vec<ObjectGroup>, + /// The custom properties of this map. pub properties: Properties, - pub background_colour: Option<Colour>, + /// The background color of this map, if any. + pub background_color: Option<Color>, pub infinite: bool, } impl Map { - pub(crate) fn new<R: Read>( + /// Parse a buffer hopefully containing the contents of a Tiled file and try to + /// parse it. This augments `parse_file` with a custom reader: some engines + /// (e.g. Amethyst) simply hand over a byte stream (and file location) for parsing, + /// in which case this function may be required. + /// + /// The path is used for external dependencies such as tilesets or images, and may be skipped + /// if the map is fully embedded (Doesn't refer to external files). If a map *does* refer to + /// external files and a path is not given, the function will return [TiledError::SourceRequired]. + pub fn parse_reader<R: Read>(reader: R, path: Option<&Path>) -> Result<Self, TiledError> { + let mut parser = EventReader::new(reader); + loop { + match parser.next().map_err(TiledError::XmlDecodingError)? { + XmlEvent::StartElement { + name, attributes, .. + } => { + if name.local_name == "map" { + return Self::parse_xml(&mut parser, attributes, path); + } + } + XmlEvent::EndDocument => { + return Err(TiledError::PrematureEnd( + "Document ended before map was parsed".to_string(), + )) + } + _ => {} + } + } + } + + /// Parse a file hopefully containing a Tiled map and try to parse it. All external + /// files will be loaded relative to the path given. + pub fn parse_file(path: impl AsRef<Path>) -> Result<Self, TiledError> { + let file = File::open(path.as_ref()) + .map_err(|_| TiledError::Other(format!("Map file not found: {:?}", path.as_ref())))?; + Self::parse_reader(file, Some(path.as_ref())) + } + + fn parse_xml<R: Read>( parser: &mut EventReader<R>, attrs: Vec<OwnedAttribute>, map_path: Option<&Path>, @@ -54,6 +98,8 @@ impl Map { TiledError::MalformedAttributes("map must have a version, width and height with correct types".to_string()) ); + let source_path = map_path.and_then(|p| p.parent()); + let mut tilesets = Vec::new(); let mut layers = Vec::new(); let mut image_layers = Vec::new(); @@ -62,7 +108,7 @@ impl Map { let mut layer_index = 0; parse_tag!(parser, "map", { "tileset" => |attrs| { - tilesets.push(Tileset::new(parser, attrs, map_path)?); + tilesets.push(Tileset::parse_xml(parser, attrs, source_path)?); Ok(()) }, "layer" => |attrs| { @@ -71,7 +117,7 @@ impl Map { Ok(()) }, "imagelayer" => |attrs| { - image_layers.push(ImageLayer::new(parser, attrs, layer_index)?); + image_layers.push(ImageLayer::new(parser, attrs, layer_index, source_path)?); layer_index += 1; Ok(()) }, @@ -97,13 +143,13 @@ impl Map { image_layers, object_groups, properties, - background_colour: c, + background_color: c, infinite: infinite.unwrap_or(false), }) } /// This function will return the correct Tileset given a GID. - pub fn get_tileset_by_gid(&self, gid: u32) -> Option<&Tileset> { + pub fn tileset_by_gid(&self, gid: u32) -> Option<&Tileset> { let mut maximum_gid: i32 = -1; let mut maximum_ts = None; for tileset in self.tilesets.iter() { @@ -116,6 +162,7 @@ impl Map { } } +/// Represents the way tiles are laid out in a map. #[derive(Debug, PartialEq, Eq, Copy, Clone)] pub enum Orientation { Orthogonal, diff --git a/src/objects.rs b/src/objects.rs index 3b66e1752633b4dd8965b94b5e81e360e11bcbca..9f4a9938c6d9e93eed74aab3d802cd39009380c1 100644 --- a/src/objects.rs +++ b/src/objects.rs @@ -4,7 +4,7 @@ use xml::{attribute::OwnedAttribute, EventReader}; use crate::{ error::TiledError, - properties::{parse_properties, Colour, Properties}, + properties::{parse_properties, Color, Properties}, util::{get_attrs, parse_tag}, }; @@ -14,7 +14,7 @@ pub struct ObjectGroup { pub opacity: f32, pub visible: bool, pub objects: Vec<Object>, - pub colour: Option<Colour>, + pub colour: Option<Color>, /** * Layer index is not preset for tile collision boxes */ diff --git a/src/properties.rs b/src/properties.rs index 68afa62bfee52abea5f3552dc2deba26ed34453d..7e8c0bd995e5675785f61216a2f3b1bd739ab481 100644 --- a/src/properties.rs +++ b/src/properties.rs @@ -8,31 +8,31 @@ use crate::{ }; #[derive(Debug, PartialEq, Eq, Copy, Clone)] -pub struct Colour { +pub struct Color { pub red: u8, pub green: u8, pub blue: u8, } -impl FromStr for Colour { +impl FromStr for Color { type Err = ParseTileError; - fn from_str(s: &str) -> Result<Colour, ParseTileError> { + fn from_str(s: &str) -> Result<Color, ParseTileError> { let s = if s.starts_with("#") { &s[1..] } else { s }; if s.len() != 6 { - return Err(ParseTileError::ColourError); + return Err(ParseTileError::ColorError); } let r = u8::from_str_radix(&s[0..2], 16); let g = u8::from_str_radix(&s[2..4], 16); let b = u8::from_str_radix(&s[4..6], 16); if r.is_ok() && g.is_ok() && b.is_ok() { - return Ok(Colour { + return Ok(Color { red: r.unwrap(), green: g.unwrap(), blue: b.unwrap(), }); } - Err(ParseTileError::ColourError) + Err(ParseTileError::ColorError) } } diff --git a/src/tile.rs b/src/tile.rs index 80d6f979ea1c89f462b013c3f5ef7c4b20369aae..4d14d2f3551ef0108c17399c76d7a1e418cfa82f 100644 --- a/src/tile.rs +++ b/src/tile.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, io::Read}; +use std::{collections::HashMap, io::Read, path::Path}; use xml::{attribute::OwnedAttribute, EventReader}; @@ -14,7 +14,7 @@ use crate::{ #[derive(Debug, PartialEq, Clone)] pub struct Tile { pub id: u32, - pub images: Vec<Image>, + pub image: Option<Image>, pub properties: Properties, pub objectgroup: Option<ObjectGroup>, pub animation: Option<Vec<Frame>>, @@ -26,6 +26,7 @@ impl Tile { pub(crate) fn new<R: Read>( parser: &mut EventReader<R>, attrs: Vec<OwnedAttribute>, + path_relative_to: Option<&Path>, ) -> Result<Tile, TiledError> { let ((tile_type, probability), id) = get_attrs!( attrs, @@ -39,13 +40,13 @@ impl Tile { TiledError::MalformedAttributes("tile must have an id with the correct type".to_string()) ); - let mut images = Vec::new(); + let mut image = Option::None; let mut properties = HashMap::new(); let mut objectgroup = None; let mut animation = None; parse_tag!(parser, "tile", { "image" => |attrs| { - images.push(Image::new(parser, attrs)?); + image = Some(Image::new(parser, attrs, path_relative_to.ok_or(TiledError::SourceRequired{object_to_parse: "Image".to_owned()})?)?); Ok(()) }, "properties" => |_| { @@ -63,7 +64,7 @@ impl Tile { }); Ok(Tile { id, - images, + image, properties, objectgroup, animation, diff --git a/src/tileset.rs b/src/tileset.rs index fe45353523aea6176296d6718ccd682e0688b647..2b120ecd9b53f4c45085cb9d75c29a0ac7571f45 100644 --- a/src/tileset.rs +++ b/src/tileset.rs @@ -1,11 +1,22 @@ +use std::collections::HashMap; +use std::fs::File; +use std::io::Read; +use std::path::{Path, PathBuf}; + +use xml::attribute::OwnedAttribute; +use xml::reader::XmlEvent; +use xml::EventReader; + +use crate::error::TiledError; +use crate::image::Image; use crate::properties::{parse_properties, Properties}; +use crate::tile::Tile; use crate::util::*; -use crate::*; // FIXME /// A tileset, usually the tilesheet image. #[derive(Debug, PartialEq, Clone)] pub struct Tileset { - /// The GID of the first tile stored + /// The GID of the first tile stored. pub first_gid: u32, pub name: String, pub tile_width: u32, @@ -13,32 +24,120 @@ pub struct Tileset { pub spacing: u32, pub margin: u32, pub tilecount: Option<u32>, - /// The Tiled spec says that a tileset can have mutliple images so a `Vec` - /// is used. Usually you will only use one. - pub images: Vec<Image>, + pub columns: u32, + + /// A tileset can either: + /// * have a single spritesheet `image` in `tileset` ("regular" tileset); + /// * have zero images in `tileset` and one `image` per `tile` ("image collection" tileset). + /// + /// -------- + /// - Source: [tiled issue #2117](https://github.com/mapeditor/tiled/issues/2117) + /// - Source: [`columns` documentation](https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#tileset) + pub image: Option<Image>, pub tiles: Vec<Tile>, pub properties: Properties, + + /// Where this tileset was loaded from. + /// If fully embedded (loaded with path = `None`), this will return `None`. + pub source: Option<PathBuf>, +} + +/// Internal structure for holding mid-parse information. +struct TilesetProperties { + spacing: Option<u32>, + margin: Option<u32>, + tilecount: Option<u32>, + columns: Option<u32>, + first_gid: u32, + name: String, + tile_width: u32, + tile_height: u32, + path_relative_to: Option<PathBuf>, + source: Option<PathBuf>, } impl Tileset { - pub(crate) fn new<R: Read>( + /// Parse a buffer hopefully containing the contents of a Tiled tileset. + /// + /// External tilesets do not have a firstgid attribute. That lives in the + /// map. You must pass in `first_gid`. If you do not need to use gids for anything, + /// passing in 1 will work fine. + pub fn parse<R: Read>(reader: R, first_gid: u32) -> Result<Self, TiledError> { + Tileset::new_external(reader, first_gid, None) + } + + /// Parse a buffer hopefully containing the contents of a Tiled tileset. + /// + /// External tilesets do not have a firstgid attribute. That lives in the + /// map. You must pass in `first_gid`. If you do not need to use gids for anything, + /// passing in 1 will work fine. + pub fn parse_with_path<R: Read>( + reader: R, + first_gid: u32, + path: impl AsRef<Path>, + ) -> Result<Self, TiledError> { + Tileset::new_external(reader, first_gid, Some(path.as_ref())) + } + + pub(crate) fn parse_xml<R: Read>( parser: &mut EventReader<R>, attrs: Vec<OwnedAttribute>, - map_path: Option<&Path>, + path_relative_to: Option<&Path>, ) -> Result<Tileset, TiledError> { - Tileset::new_internal(parser, &attrs).or_else(|_| Tileset::new_reference(&attrs, map_path)) + Tileset::parse_xml_embedded(parser, &attrs, path_relative_to).or_else(|err| { + if matches!(err, TiledError::MalformedAttributes(_)) { + Tileset::parse_xml_reference(&attrs, path_relative_to) + } else { + Err(err) + } + }) } - fn new_internal<R: Read>( + pub(crate) fn new_external<R: Read>( + file: R, + first_gid: u32, + path: Option<&Path>, + ) -> Result<Self, TiledError> { + let mut tileset_parser = EventReader::new(file); + loop { + match tileset_parser + .next() + .map_err(TiledError::XmlDecodingError)? + { + XmlEvent::StartElement { + name, attributes, .. + } => { + if name.local_name == "tileset" { + return Self::parse_external_tileset( + first_gid, + &mut tileset_parser, + &attributes, + path, + ); + } + } + XmlEvent::EndDocument => { + return Err(TiledError::PrematureEnd( + "Tileset Document ended before map was parsed".to_string(), + )) + } + _ => {} + } + } + } + + fn parse_xml_embedded<R: Read>( parser: &mut EventReader<R>, attrs: &Vec<OwnedAttribute>, + path_relative_to: Option<&Path>, ) -> Result<Tileset, TiledError> { - let ((spacing, margin, tilecount), (first_gid, name, width, height)) = get_attrs!( + let ((spacing, margin, tilecount, columns), (first_gid, name, tile_width, tile_height)) = get_attrs!( attrs, optionals: [ ("spacing", spacing, |v:String| v.parse().ok()), ("margin", margin, |v:String| v.parse().ok()), ("tilecount", tilecount, |v:String| v.parse().ok()), + ("columns", columns, |v:String| v.parse().ok()), ], required: [ ("firstgid", first_gid, |v:String| v.parse().ok()), @@ -49,41 +148,26 @@ impl Tileset { TiledError::MalformedAttributes("tileset must have a firstgid, name tile width and height with correct types".to_string()) ); - let mut images = Vec::new(); - let mut tiles = Vec::new(); - let mut properties = HashMap::new(); - parse_tag!(parser, "tileset", { - "image" => |attrs| { - images.push(Image::new(parser, attrs)?); - Ok(()) - }, - "properties" => |_| { - properties = parse_properties(parser)?; - Ok(()) - }, - "tile" => |attrs| { - tiles.push(Tile::new(parser, attrs)?); - Ok(()) + Self::finish_parsing_xml( + parser, + TilesetProperties { + spacing, + margin, + name, + path_relative_to: path_relative_to.map(Path::to_owned), + columns, + tilecount, + tile_height, + tile_width, + first_gid, + source: None, }, - }); - - Ok(Tileset { - tile_width: width, - tile_height: height, - spacing: spacing.unwrap_or(0), - margin: margin.unwrap_or(0), - first_gid, - name, - tilecount, - images, - tiles, - properties, - }) + ) } - fn new_reference( + fn parse_xml_reference( attrs: &Vec<OwnedAttribute>, - map_path: Option<&Path>, + path_relative_to: Option<&Path>, ) -> Result<Tileset, TiledError> { let ((), (first_gid, source)) = get_attrs!( attrs, @@ -92,58 +176,36 @@ impl Tileset { ("firstgid", first_gid, |v:String| v.parse().ok()), ("source", name, |v| Some(v)), ], - TiledError::MalformedAttributes("tileset must have a firstgid, name tile width and height with correct types".to_string()) + TiledError::MalformedAttributes("Tileset reference must have a firstgid and source with correct types".to_string()) ); - let tileset_path = map_path.ok_or(TiledError::Other("Maps with external tilesets must know their file location. See parse_with_path(Path).".to_string()))?.with_file_name(source); + let tileset_path = path_relative_to + .ok_or(TiledError::SourceRequired { + object_to_parse: "Tileset".to_string(), + })? + .join(source); let file = File::open(&tileset_path).map_err(|_| { TiledError::Other(format!( "External tileset file not found: {:?}", tileset_path )) })?; - Tileset::new_external(file, first_gid) - } - - pub(crate) fn new_external<R: Read>(file: R, first_gid: u32) -> Result<Tileset, TiledError> { - let mut tileset_parser = EventReader::new(file); - loop { - match tileset_parser - .next() - .map_err(TiledError::XmlDecodingError)? - { - XmlEvent::StartElement { - name, attributes, .. - } => { - if name.local_name == "tileset" { - return Tileset::parse_external_tileset( - first_gid, - &mut tileset_parser, - &attributes, - ); - } - } - XmlEvent::EndDocument => { - return Err(TiledError::PrematureEnd( - "Tileset Document ended before map was parsed".to_string(), - )) - } - _ => {} - } - } + Tileset::new_external(file, first_gid, Some(&tileset_path)) } fn parse_external_tileset<R: Read>( first_gid: u32, parser: &mut EventReader<R>, attrs: &Vec<OwnedAttribute>, + path: Option<&Path>, ) -> Result<Tileset, TiledError> { - let ((spacing, margin, tilecount), (name, width, height)) = get_attrs!( + let ((spacing, margin, tilecount, columns), (name, tile_width, tile_height)) = get_attrs!( attrs, optionals: [ ("spacing", spacing, |v:String| v.parse().ok()), ("margin", margin, |v:String| v.parse().ok()), ("tilecount", tilecount, |v:String| v.parse().ok()), + ("columns", columns, |v:String| v.parse().ok()), ], required: [ ("name", name, |v| Some(v)), @@ -153,35 +215,81 @@ impl Tileset { TiledError::MalformedAttributes("tileset must have a firstgid, name tile width and height with correct types".to_string()) ); - let mut images = Vec::new(); + let source_path = path.and_then(|p| p.parent().map(Path::to_owned)); + + Self::finish_parsing_xml( + parser, + TilesetProperties { + spacing, + margin, + name, + path_relative_to: source_path, + columns, + tilecount, + tile_height, + tile_width, + first_gid, + source: path.map(Path::to_owned), + }, + ) + } + + fn finish_parsing_xml<R: Read>( + parser: &mut EventReader<R>, + prop: TilesetProperties, + ) -> Result<Self, TiledError> { + let mut image = Option::None; let mut tiles = Vec::new(); let mut properties = HashMap::new(); parse_tag!(parser, "tileset", { "image" => |attrs| { - images.push(Image::new(parser, attrs)?); - Ok(()) - }, - "tile" => |attrs| { - tiles.push(Tile::new(parser, attrs)?); + image = Some(Image::new(parser, attrs, prop.path_relative_to.as_ref().ok_or(TiledError::SourceRequired{object_to_parse: "Image".to_string()})?)?); Ok(()) }, "properties" => |_| { properties = parse_properties(parser)?; Ok(()) }, + "tile" => |attrs| { + tiles.push(Tile::new(parser, attrs, prop.path_relative_to.as_ref().and_then(|p| Some(p.as_path())))?); + Ok(()) + }, }); + let (margin, spacing) = (prop.margin.unwrap_or(0), prop.spacing.unwrap_or(0)); + + let columns = prop + .columns + .map(Ok) + .unwrap_or_else(|| Self::calculate_columns(&image, prop.tile_width, margin, spacing))?; + Ok(Tileset { - first_gid: first_gid, - name: name, - tile_width: width, - tile_height: height, - spacing: spacing.unwrap_or(0), - margin: margin.unwrap_or(0), - tilecount: tilecount, - images: images, - tiles: tiles, + first_gid: prop.first_gid, + name: prop.name, + tile_width: prop.tile_width, + tile_height: prop.tile_height, + spacing, + margin, + columns, + tilecount: prop.tilecount, + image, + tiles, properties, + source: prop.source, }) } + + fn calculate_columns( + image: &Option<Image>, + tile_width: u32, + margin: u32, + spacing: u32, + ) -> Result<u32, TiledError> { + image + .as_ref() + .ok_or(TiledError::MalformedAttributes( + "No <image> nor columns attribute in <tileset>".to_string(), + )) + .and_then(|image| Ok((image.width as u32 - margin + spacing) / (tile_width + spacing))) + } } diff --git a/src/util.rs b/src/util.rs index 61dcfd1c24bf24cdbebaba82fe17da8e93d048b4..50d28e5ebf87d4282ad27ef3b52fb54c677331c2 100644 --- a/src/util.rs +++ b/src/util.rs @@ -26,9 +26,7 @@ macro_rules! get_attrs { } /// Goes through the children of the tag and will call the correct function for -/// that child. Closes the tag -/// -/// Not quite as bad. +/// that child. Closes the tag. macro_rules! parse_tag { ($parser:expr, $close_tag:expr, {$($open_tag:expr => $open_method:expr),* $(,)*}) => { loop { @@ -56,9 +54,7 @@ macro_rules! parse_tag { use std::{ collections::HashMap, - fs::File, io::{BufReader, Read}, - path::Path, }; pub(crate) use get_attrs; @@ -69,8 +65,6 @@ use crate::{ animation::Frame, error::TiledError, layers::{Chunk, LayerData, LayerTile}, - map::Map, - tileset::Tileset, }; pub(crate) fn parse_animation<R: Read>( @@ -89,7 +83,6 @@ pub(crate) fn parse_animation<R: Read>( pub(crate) fn parse_infinite_data<R: Read>( parser: &mut EventReader<R>, attrs: Vec<OwnedAttribute>, - width: u32, ) -> Result<LayerData, TiledError> { let ((e, c), ()) = get_attrs!( attrs, @@ -278,24 +271,3 @@ pub(crate) fn convert_to_tile(all: &Vec<u8>, width: u32) -> Vec<Vec<LayerTile>> } data } - -pub(crate) fn parse_impl<R: Read>(reader: R, map_path: Option<&Path>) -> Result<Map, TiledError> { - let mut parser = EventReader::new(reader); - loop { - match parser.next().map_err(TiledError::XmlDecodingError)? { - XmlEvent::StartElement { - name, attributes, .. - } => { - if name.local_name == "map" { - return Map::new(&mut parser, attributes, map_path); - } - } - XmlEvent::EndDocument => { - return Err(TiledError::PrematureEnd( - "Document ended before map was parsed".to_string(), - )) - } - _ => {} - } - } -} \ No newline at end of file diff --git a/tests/lib.rs b/tests/lib.rs index c46a54e904eb5e9d15592fc458b3242ff0f66791..eb4aa6d7f5cb3381fbd695898d32e90b6013f4c0 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -1,26 +1,21 @@ -use std::fs::File; use std::path::Path; +use std::{fs::File, path::PathBuf}; use tiled::{ - error::TiledError, layers::LayerData, map::Map, parse, parse_file, parse_tileset, - properties::PropertyValue, + error::TiledError, layers::LayerData, map::Map, properties::PropertyValue, tileset::Tileset, }; -fn read_from_file(p: &Path) -> Result<Map, TiledError> { +fn parse_map_without_source(p: impl AsRef<Path>) -> Result<Map, TiledError> { let file = File::open(p).unwrap(); - return parse(file); -} - -fn read_from_file_with_path(p: &Path) -> Result<Map, TiledError> { - return parse_file(p); + return Map::parse_reader(file, None); } #[test] fn test_gzip_and_zlib_encoded_and_raw_are_the_same() { - let z = read_from_file(&Path::new("assets/tiled_base64_zlib.tmx")).unwrap(); - let g = read_from_file(&Path::new("assets/tiled_base64_gzip.tmx")).unwrap(); - let r = read_from_file(&Path::new("assets/tiled_base64.tmx")).unwrap(); - let zstd = read_from_file(&Path::new("assets/tiled_base64_zstandard.tmx")).unwrap(); - let c = read_from_file(&Path::new("assets/tiled_csv.tmx")).unwrap(); + let z = Map::parse_file("assets/tiled_base64_zlib.tmx").unwrap(); + let g = Map::parse_file("assets/tiled_base64_gzip.tmx").unwrap(); + let r = Map::parse_file("assets/tiled_base64.tmx").unwrap(); + let zstd = Map::parse_file("assets/tiled_base64_zstandard.tmx").unwrap(); + let c = Map::parse_file("assets/tiled_csv.tmx").unwrap(); assert_eq!(z, g); assert_eq!(z, r); assert_eq!(z, c); @@ -42,21 +37,36 @@ fn test_gzip_and_zlib_encoded_and_raw_are_the_same() { #[test] fn test_external_tileset() { - let r = read_from_file(&Path::new("assets/tiled_base64.tmx")).unwrap(); - let e = read_from_file_with_path(&Path::new("assets/tiled_base64_external.tmx")).unwrap(); + let r = Map::parse_file("assets/tiled_base64.tmx").unwrap(); + let mut e = Map::parse_file("assets/tiled_base64_external.tmx").unwrap(); + e.tilesets[0].source = None; assert_eq!(r, e); } +#[test] +fn test_sources() { + let e = Map::parse_file("assets/tiled_base64_external.tmx").unwrap(); + assert_eq!( + e.tilesets[0].source, + Some(PathBuf::from("assets/tilesheet.tsx")) + ); + assert_eq!( + e.tilesets[0].image.as_ref().unwrap().source, + PathBuf::from("assets/tilesheet.png") + ); +} + #[test] fn test_just_tileset() { - let r = read_from_file(&Path::new("assets/tiled_base64.tmx")).unwrap(); - let t = parse_tileset(File::open(Path::new("assets/tilesheet.tsx")).unwrap(), 1).unwrap(); + let r = Map::parse_file("assets/tiled_base64_external.tmx").unwrap(); + let path = "assets/tilesheet.tsx"; + let t = Tileset::parse_with_path(File::open(path).unwrap(), 1, path).unwrap(); assert_eq!(r.tilesets[0], t); } #[test] fn test_infinite_tileset() { - let r = read_from_file_with_path(&Path::new("assets/tiled_base64_zlib_infinite.tmx")).unwrap(); + let r = Map::parse_file("assets/tiled_base64_zlib_infinite.tmx").unwrap(); if let LayerData::Infinite(chunks) = &r.layers[0].tiles { assert_eq!(chunks.len(), 4); @@ -73,7 +83,7 @@ fn test_infinite_tileset() { #[test] fn test_image_layers() { - let r = read_from_file(&Path::new("assets/tiled_image_layers.tmx")).unwrap(); + let r = Map::parse_file("assets/tiled_image_layers.tmx").unwrap(); assert_eq!(r.image_layers.len(), 2); { let first = &r.image_layers[0]; @@ -91,7 +101,7 @@ fn test_image_layers() { .image .as_ref() .expect(&format!("{}'s image shouldn't be None", second.name)); - assert_eq!(image.source, "tilesheet.png"); + assert_eq!(image.source, PathBuf::from("assets/tilesheet.png")); assert_eq!(image.width, 448); assert_eq!(image.height, 192); } @@ -99,7 +109,7 @@ fn test_image_layers() { #[test] fn test_tile_property() { - let r = read_from_file(&Path::new("assets/tiled_base64.tmx")).unwrap(); + let r = Map::parse_file("assets/tiled_base64.tmx").unwrap(); let prop_value: String = if let Some(&PropertyValue::StringValue(ref v)) = r.tilesets[0].tiles[0].properties.get("a tile property") { @@ -112,20 +122,19 @@ fn test_tile_property() { #[test] fn test_layer_property() { - let r = read_from_file(&Path::new("assets/tiled_base64.tmx")).unwrap(); - let prop_value: String = if let Some(&PropertyValue::StringValue(ref v)) = - r.layers[0].properties.get("prop3") - { - v.clone() - } else { - String::new() - }; + let r = Map::parse_file(&Path::new("assets/tiled_base64.tmx")).unwrap(); + let prop_value: String = + if let Some(&PropertyValue::StringValue(ref v)) = r.layers[0].properties.get("prop3") { + v.clone() + } else { + String::new() + }; assert_eq!("Line 1\r\nLine 2\r\nLine 3,\r\n etc\r\n ", prop_value); } #[test] fn test_object_group_property() { - let r = read_from_file(&Path::new("assets/tiled_object_groups.tmx")).unwrap(); + let r = Map::parse_file("assets/tiled_object_groups.tmx").unwrap(); let prop_value: bool = if let Some(&PropertyValue::BoolValue(ref v)) = r.object_groups[0] .properties .get("an object group property") @@ -138,7 +147,7 @@ fn test_object_group_property() { } #[test] fn test_tileset_property() { - let r = read_from_file(&Path::new("assets/tiled_base64.tmx")).unwrap(); + let r = Map::parse_file("assets/tiled_base64.tmx").unwrap(); let prop_value: String = if let Some(&PropertyValue::StringValue(ref v)) = r.tilesets[0].properties.get("tileset property") { @@ -151,7 +160,7 @@ fn test_tileset_property() { #[test] fn test_flipped_gid() { - let r = read_from_file_with_path(&Path::new("assets/tiled_flipped.tmx")).unwrap(); + let r = Map::parse_file("assets/tiled_flipped.tmx").unwrap(); if let LayerData::Finite(tiles) = &r.layers[0].tiles { let t1 = tiles[0][0]; @@ -180,7 +189,7 @@ fn test_flipped_gid() { #[test] fn test_ldk_export() { - let r = read_from_file_with_path(&Path::new("assets/ldk_tiled_export.tmx")).unwrap(); + let r = Map::parse_file("assets/ldk_tiled_export.tmx").unwrap(); if let LayerData::Finite(tiles) = &r.layers[0].tiles { assert_eq!(tiles.len(), 8); assert_eq!(tiles[0].len(), 8); @@ -193,9 +202,10 @@ fn test_ldk_export() { #[test] fn test_object_property() { - let r = read_from_file(&Path::new("assets/tiled_object_property.tmx")).unwrap(); - let prop_value = if let Some(PropertyValue::ObjectValue(v)) = - r.object_groups[0].objects[0].properties.get("object property") + let r = parse_map_without_source(&Path::new("assets/tiled_object_property.tmx")).unwrap(); + let prop_value = if let Some(PropertyValue::ObjectValue(v)) = r.object_groups[0].objects[0] + .properties + .get("object property") { *v } else {