From d775eb1e9c6be67e5c07cabde79145b4ed3136e1 Mon Sep 17 00:00:00 2001 From: LawnCable Date: Fri, 15 Apr 2022 08:37:04 +0200 Subject: improvements, wangset and images alse parsing for may tileset related types --- readme.md | 21 +- src/lowlevel/data.rs | 7 +- src/lowlevel/image.rs | 139 ++++++++++++++ src/lowlevel/mod.rs | 2 + src/lowlevel/property.rs | 17 +- src/lowlevel/tileset.rs | 441 ++++++++++++++++++++++++++++++++++++------ src/lowlevel/wangset.rs | 484 +++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 1035 insertions(+), 76 deletions(-) create mode 100644 src/lowlevel/wangset.rs diff --git a/readme.md b/readme.md index 0eacb82..25fefd5 100644 --- a/readme.md +++ b/readme.md @@ -18,6 +18,7 @@ Wishlist (things that would be nice): - serializing from rust types back to tmx/tsx (this needs including another xml lib, because roxmltree if fast because it is read-only) - parsing from and serializing back to the tiled JSON based formats +- maybe later support parsing older formats ## Thanks to @@ -27,7 +28,6 @@ Wishlist (things that would be nice): Move: version="1.4" tiledversion="1.4.3" to some kind of file-wrapper type? Because it is also present in tilesets? Or just add those two to tileset and inherit it if its inline/embedded. - ## progress | element | rust representation | parse from XML | tests for parse | @@ -36,21 +36,22 @@ Move: version="1.4" tiledversion="1.4.3" to some kind of file-wrapper type? Beca | `` | - | - | - | | `.` | - | - | - | | `.` | - | - | - | -| `` | partial | - | - | +| `` | complete | - | - | | `.` | complete | complete | complete | | `.` | complete | complete | complete | -| `.` | partial | - | - | +| `.` | partial | partial | partial | +| `..image in ` | partial | - | - | | `.` | complete | - | - | | `..` | complete | complete | - | | `.` | complete | complete | complete | -| `.` | partial | - | - | -| `..` | complete | - | - | -| `.` | complete | - | - | +| `.` | partial (5/6) | partial (5/6) | partial (3/6) | +| `..` | complete | complete | complete | +| `.` | complete | complete | complete | | `.` | partial | - | - | -| `..` | partial | - | - | -| `...` | complete | - | - | -| `...` | partial | - | - | -| `....Wang ID` | - | - | - | +| `..` | complete | complete | complete | +| `...` | complete | complete | complete | +| `...` | complete | complete | complete | +| `....Wang ID` | complete | complete | complete | | `` | - | - | - | | `.` | - | - | - | | `.` | - | - | - | diff --git a/src/lowlevel/data.rs b/src/lowlevel/data.rs index 7b64a12..e2833b5 100644 --- a/src/lowlevel/data.rs +++ b/src/lowlevel/data.rs @@ -1,11 +1,13 @@ struct LayerData {} - +#[derive(Debug, PartialEq)] /// The encoding used to encode the tile layer data. When used, it can be “base64” and “csv” at the moment. pub enum Encoding { + /// Plain XML based, in tags + XML, Base64, CSV, } - +#[derive(Debug, PartialEq)] /// The compression used to compress the tile layer data. /// Tiled supports “gzip”, “zlib” and (as a compile-time option since Tiled 1.3) “zstd”. pub enum Compression { @@ -15,6 +17,7 @@ pub enum Compression { Zstd, } +#[derive(Debug, PartialEq)] pub struct EncodedData { pub encoding: Encoding, pub compression: Compression, diff --git a/src/lowlevel/image.rs b/src/lowlevel/image.rs index 5374b8c..6747e69 100644 --- a/src/lowlevel/image.rs +++ b/src/lowlevel/image.rs @@ -1,5 +1,8 @@ +use crate::{ensure_element, ensure_tag_name, get_attribute}; +use anyhow::anyhow; use rgb::RGB8; +#[derive(Debug, PartialEq)] pub enum ImageSource { External { /// The reference to the tileset image file (Tiled supports most common image formats). Only used if the image is not embedded. @@ -16,6 +19,7 @@ pub enum ImageSource { }, } +#[derive(Debug, PartialEq)] pub struct Image { pub source: ImageSource, /// Defines a specific color that is treated as transparent (example value: “#FF00FF” for magenta). @@ -26,3 +30,138 @@ pub struct Image { /// The image height in pixels (optional) pub height: Option, } + +impl Image { + pub fn from_xml(node: roxmltree::Node) -> anyhow::Result { + ensure_element!(node); + ensure_tag_name!(node, "image"); + + let width: Option = match node.attribute("width") { + Some(s) => Some(s.parse()?), + None => None, + }; + + let height: Option = match node.attribute("height") { + Some(s) => Some(s.parse()?), + None => None, + }; + + let transparent_color: Option = if let Some(s) = node.attribute("trans") { + { + let mut c = s.trim_start_matches("#").chars(); + if let Some(color) = read_color::rgb(&mut c) { + Ok(Some(RGB8 { + r: color[0], + g: color[1], + b: color[2], + })) + } else { + Err(anyhow!("could not parse color")) + } + }? + } else { + None + }; + + let image_source = match node.attribute("source") { + Some(s) => Ok(ImageSource::External { + source: s.to_owned(), + }), + None => { + // embedded + let _format = get_attribute!(node, "format")?; + + // let xmldata_node = match node.children().find(|&c| c.has_tag_name("data")) { + // Some(node) => super::data::EncodedData::from_xml(node)?, + // None => Err(anyhow!("data element of embeded image not found")), + // }; + + // TODO + + Err(anyhow!("embeded images are not implemented yet")) + } + }?; + Ok(Image { + source: image_source, + transparent_color, + width, + height, + }) + } +} + +#[cfg(test)] +mod parse_image { + use crate::lowlevel::image::*; + use crate::parse_property_test; + + #[test] + fn image_with_dimensions() { + parse_property_test!( + Image, + r##""##, + Image { + source: ImageSource::External { + source: "beach_tileset.png".to_owned() + }, + transparent_color: None, + width: Some(576), + height: Some(416) + } + ); + } + + #[test] + fn image_without_dimensions() { + parse_property_test!( + Image, + r##""##, + Image { + source: ImageSource::External { + source: "perspective_walls.png".to_owned() + }, + transparent_color: None, + width: None, + height: None + } + ); + } + + #[test] + fn image_with_transparency_color() { + parse_property_test!( + Image, + r##""##, + Image { + source: ImageSource::External { + source: "sewer_tileset.png".to_owned() + }, + transparent_color: Some(RGB8 { + r: 255, + g: 0, + b: 255 + }), + width: Some(192), + height: Some(217) + } + ); + parse_property_test!( + Image, + r##""##, + Image { + source: ImageSource::External { + source: "sewer_tileset.png".to_owned() + }, + transparent_color: Some(RGB8 { + r: 255, + g: 0, + b: 255 + }), + width: Some(192), + height: Some(217) + } + ); + } + + // TODO test embeded image +} diff --git a/src/lowlevel/mod.rs b/src/lowlevel/mod.rs index 34d0fb7..824c693 100644 --- a/src/lowlevel/mod.rs +++ b/src/lowlevel/mod.rs @@ -11,3 +11,5 @@ pub mod data; pub mod layer; pub(crate) mod macros; + +pub mod wangset; \ No newline at end of file diff --git a/src/lowlevel/property.rs b/src/lowlevel/property.rs index bba693f..def204d 100644 --- a/src/lowlevel/property.rs +++ b/src/lowlevel/property.rs @@ -26,7 +26,10 @@ impl Property { ensure_tag_name!(node, "property"); let name = get_attribute!(node, "name")?.to_owned(); - let property_type = get_attribute!(node, "type")?; + let property_type = match node.attribute("type") { + Some(ty) => ty, + None => "string", + }; // handle the case that 'string' value is stored in element content instead of value atribute if property_type == "string" { @@ -274,6 +277,18 @@ mod parse_property { } ); } + + #[test] + fn string_without_explicit_type() { + crate::parse_property_test!( + Property, + r##""##, + Property::String { + name: "door".to_owned(), + value: "true".to_owned() + } + ); + } } #[cfg(test)] diff --git a/src/lowlevel/tileset.rs b/src/lowlevel/tileset.rs index 0e6dc87..9db86c8 100644 --- a/src/lowlevel/tileset.rs +++ b/src/lowlevel/tileset.rs @@ -1,9 +1,12 @@ use crate::{ensure_element, ensure_tag_name, get_attribute}; use anyhow::anyhow; -use rgb::RGB8; +use super::image::Image; use super::property::Properties; +use super::wangset::WangSet; + +#[derive(Debug, PartialEq)] pub enum ObjectAlignment { Unspecified, TopLeft, @@ -23,6 +26,27 @@ impl Default for ObjectAlignment { } } +impl ObjectAlignment { + pub fn from_string(string: &str) -> anyhow::Result { + match string { + "unspecified" => Ok(ObjectAlignment::Unspecified), + "topleft" => Ok(ObjectAlignment::TopLeft), + "top" => Ok(ObjectAlignment::Top), + "topright" => Ok(ObjectAlignment::TopRight), + "left" => Ok(ObjectAlignment::Left), + "center" => Ok(ObjectAlignment::Center), + "right" => Ok(ObjectAlignment::Right), + "bottomleft" => Ok(ObjectAlignment::BottomLeft), + "bottom" => Ok(ObjectAlignment::Bottom), + "bottomright" => Ok(ObjectAlignment::BottomRight), + _ => Err(anyhow!( + "object orientation \"{}\" is not supported", + string + )), + } + } +} +#[derive(Debug, PartialEq)] pub struct Frame { /// The local ID of a tile within the parent . tileid: u32, @@ -31,7 +55,80 @@ pub struct Frame { duration: u32, } +impl Frame { + pub fn from_xml(node: roxmltree::Node) -> anyhow::Result { + ensure_element!(node); + ensure_tag_name!(node, "frame"); + Ok(Frame { + tileid: get_attribute!(node, "tileid")?.parse()?, + duration: get_attribute!(node, "duration")?.parse()?, + }) + } + + pub fn from_animation_xml(node: roxmltree::Node) -> anyhow::Result> { + ensure_element!(node); + ensure_tag_name!(node, "animation"); + let mut frames: Vec = Vec::new(); + for frame_node in node.children() { + if frame_node.has_tag_name("frame") { + frames.push(Frame::from_xml(frame_node)?) + } + } + Ok(frames) + } +} +#[cfg(test)] +mod test_frame { + use crate::lowlevel::tileset::Frame; + use crate::parse_property_test; + + #[test] + fn single_frame() { + parse_property_test!( + Frame, + r##""##, + Frame { + tileid: 37, + duration: 1000 + } + ); + } + + #[test] + fn animation() { + let source = r##" + + + + + "##; + let expected_output = vec![ + Frame { + tileid: 148, + duration: 1000, + }, + Frame { + tileid: 157, + duration: 1000, + }, + Frame { + tileid: 166, + duration: 1000, + }, + Frame { + tileid: 175, + duration: 1000, + }, + ]; + + let doc = roxmltree::Document::parse(source).unwrap(); + let elem = doc.root_element(); + assert_eq!(Frame::from_animation_xml(elem).unwrap(), expected_output); + } +} + /// Define properties for a specific tile +#[derive(Debug, PartialEq)] pub struct Tile { /// The local tile ID within its tileset. pub id: u32, @@ -52,14 +149,127 @@ pub struct Tile { pub properties: super::property::Properties, - //TODO Can contain at most one: (since 0.9), , - pub image: Option, + pub image: Option, + // TODO , /// Each tile can have exactly one animation associated with it. /// In the future, there could be support for multiple named animations on a tile. pub animation: Option>, } +impl Tile { + pub fn from_xml(node: roxmltree::Node) -> anyhow::Result { + ensure_element!(node); + ensure_tag_name!(node, "tile"); + + let id: u32 = get_attribute!(node, "id")?.parse()?; + + let probability: f64 = match node.attribute("probability") { + Some(p) => p.parse()?, + None => 0f64, + }; + + let properties: Properties = match node.children().find(|&c| c.has_tag_name("properties")) { + Some(node) => Properties::from_xml(node)?, + None => Properties::default(), + }; + + let animation: Option> = + match node.children().find(|&c| c.has_tag_name("animation")) { + Some(node) => Some(Frame::from_animation_xml(node)?), + None => None, + }; + + let image = if let Some(node) = node.children().find(|&c| c.has_tag_name("image")) { + Some(Image::from_xml(node)?) + } else { + None + }; + + // TODO , + + Ok(Tile { + id, + probability, + properties, + image, + animation, + }) + } +} + +#[cfg(test)] +mod test_tile { + use crate::lowlevel::property::*; + use crate::lowlevel::tileset::{Frame, Tile}; + use crate::parse_property_test; + + #[test] + fn basic_tile() { + parse_property_test!( + Tile, + r##" + + + + "##, + Tile { + id: 13, + probability: 0.00, + properties: Properties { + properties: vec![Property::String { + name: "door".to_owned(), + value: "true".to_owned() + }] + }, + image: None, + animation: None + } + ); + } + + #[test] + fn animated_tile() { + parse_property_test!( + Tile, + r##" + + + + + + + "##, + Tile { + id: 148, + probability: 0.00, + properties: Properties::default(), + image: None, + animation: Some(vec![ + Frame { + tileid: 148, + duration: 1000, + }, + Frame { + tileid: 157, + duration: 1000, + }, + Frame { + tileid: 166, + duration: 1000, + }, + Frame { + tileid: 175, + duration: 1000, + } + ]) + } + ); + } + + // TODO test tile with image +} + /// This element is used to specify an offset in pixels, /// to be applied when drawing a tile from the related tileset. /// When not present, no offset is applied. @@ -227,66 +437,6 @@ impl Terrain { } } -/// A color that can be used to define the corner and/or edge of a Wang tile. -#[derive(PartialEq, Debug)] -pub struct WangColor { - /// The name of this color. - pub name: String, - /// The color in #RRGGBB format (example: #c17d11). - pub color: RGB8, - /// The local tile ID of the tile representing this color. - pub tile: u32, - /// The relative probability that this color is chosen over others in case of multiple options. - /// (defaults to 0) - pub probability: f64, - - // Can contain at most one: - pub properties: Properties, -} - -/// “The Wang ID, given by a comma-separated list of indexes -/// (starting from 1, because 0 means _unset_) -/// referring to the Wang colors in the Wang set in the following order: -/// top, top right, right, bottom right, bottom, bottom left, left, top left (since Tiled 1.5). -/// Before Tiled 1.5, the Wang ID was saved as a 32-bit unsigned integer -/// stored in the format 0xCECECECE (where each C is a corner color and each E is an edge color, -/// in reverse order).” -#[derive(PartialEq, Debug)] -pub struct WangId {} - -///Defines a Wang tile, by referring to a tile in the tileset and associating it with a certain Wang ID. -#[derive(PartialEq, Debug)] -pub struct WangTile { - /// The tile ID. - pub tileid: u32, - /// The Wang ID - pub wangid: WangId, - /// Whether the tile is flipped horizontally (removed in Tiled 1.5). - pub hflip: bool, - /// Whether the tile is flipped vertically (removed in Tiled 1.5). - pub vflip: bool, - /// Whether the tile is flipped on its diagonal (removed in Tiled 1.5). - pub dflip: bool, -} - -/// Defines a list of corner colors and a list of edge colors, -/// and any number of Wang tiles using these colors. -#[derive(PartialEq, Debug)] -pub struct WangSet { - ///The name of the Wang set. - pub name: String, - /// The tile ID of the tile representing this Wang set. - pub tile: u32, - - // Can contain at most one: - pub properties: Properties, - - /// Can contain up to 255: (since Tiled 1.5) - pub wangcolor: Vec, - - /// Can contain any number: - pub wangtiles: Vec, -} /// This element is used to describe which transformations can be applied to the tiles /// (e.g. to extend a Wang set by transforming existing tiles). @@ -386,6 +536,7 @@ mod test_transformations { } } +#[derive(Debug, PartialEq)] pub struct TileSet { /// The name of this tileset. pub name: String, @@ -437,6 +588,170 @@ pub struct TileSet { pub transformations: TileSetTransformations, } +impl TileSet { + pub fn from_xml(node: &roxmltree::Node) -> anyhow::Result { + let spacing: usize = match node.attribute("spacing") { + Some(s) => s.parse()?, + None => 0, + }; + + let margin: usize = match node.attribute("margin") { + Some(s) => s.parse()?, + None => 0, + }; + + let object_alignment = match node.attribute("objectalignment") { + Some(child_node) => ObjectAlignment::from_string(child_node)?, + None => ObjectAlignment::default(), + }; + + let mut tiles: Vec = Vec::new(); + for tile_node in node.children() { + if tile_node.has_tag_name("tile") { + tiles.push(Tile::from_xml(tile_node)?) + } + } + + let image = match node.children().find(|&c| c.has_tag_name("image")) { + Some(child_node) => Some(Image::from_xml(child_node)?), + None => None, + }; + + let tile_offset = match node.children().find(|&c| c.has_tag_name("tileoffset")) { + Some(tile_offset_node) => TileOffset::from_xml(tile_offset_node)?, + None => TileOffset::default(), + }; + + let grid: Option = match node.children().find(|&c| c.has_tag_name("grid")) { + Some(child_node) => Some(Grid::from_xml(child_node)?), + None => None, + }; + + let properties: Properties = match node.children().find(|&c| c.has_tag_name("properties")) { + Some(child_node) => Properties::from_xml(child_node)?, + None => Properties::default(), + }; + + let transformations = match node.children().find(|&c| c.has_tag_name("transformations")) { + Some(child_node) => TileSetTransformations::from_xml(child_node)?, + None => TileSetTransformations::default(), + }; + + Ok(TileSet { + name: get_attribute!(node, "name")?.to_owned(), + tile_width: get_attribute!(node, "tilewidth")?.parse()?, + tile_height: get_attribute!(node, "tileheight")?.parse()?, + spacing, + margin, + tile_count: get_attribute!(node, "tilecount")?.parse()?, + columns: get_attribute!(node, "columns")?.parse()?, + object_alignment, + tile: tiles, + image, + tile_offset, + grid, + properties, + terrain_types: None, // TODO + wangsets: None, // TODO + transformations, + }) + } +} + +#[cfg(test)] +mod test_tileset { + use crate::lowlevel::image::*; + use crate::lowlevel::tileset::*; + use std::fs; + + #[test] + fn example_beach_tileset() { + let file = fs::read_to_string("testing_data/tiled_examples/rpg/beach_tileset.tsx").unwrap(); + let doc = roxmltree::Document::parse(&file).unwrap(); + let elem = doc.root_element(); + let tile_set = TileSet::from_xml(&elem).unwrap(); + assert_eq!( + tile_set, + TileSet { + name: "beach_tileset".to_owned(), + tile_width: 16, + tile_height: 16, + spacing: 0, + margin: 0, + tile_count: 936, + columns: 36, + object_alignment: ObjectAlignment::Unspecified, + tile: vec![ + Tile { + id: 37, + probability: 0.0, + properties: Properties::default(), + image: None, + animation: Some(vec![ + Frame { + tileid: 37, + duration: 1000 + }, + Frame { + tileid: 46, + duration: 1000 + }, + Frame { + tileid: 55, + duration: 1000 + }, + Frame { + tileid: 64, + duration: 1000 + } + ]) + }, + Tile { + id: 148, + probability: 0.0, + properties: Properties::default(), + image: None, + animation: Some(vec![ + Frame { + tileid: 148, + duration: 1000 + }, + Frame { + tileid: 157, + duration: 1000 + }, + Frame { + tileid: 166, + duration: 1000 + }, + Frame { + tileid: 175, + duration: 1000 + } + ]) + } + ], + image: Some(Image { + source: ImageSource::External { + source: "beach_tileset.png".to_owned() + }, + transparent_color: None, + width: Some(576), + height: Some(416) + }), + tile_offset: TileOffset::default(), + grid: None, + properties: Properties::default(), + terrain_types: None, + wangsets: None, + transformations: TileSetTransformations::default() + } + ); + } + + // todo more tests +} + pub enum TileSetElement { Embedded { first_gid: usize, tileset: TileSet }, External { first_gid: usize, source: String }, diff --git a/src/lowlevel/wangset.rs b/src/lowlevel/wangset.rs new file mode 100644 index 0000000..421ccd0 --- /dev/null +++ b/src/lowlevel/wangset.rs @@ -0,0 +1,484 @@ +use crate::{ensure_element, ensure_tag_name, get_attribute}; +use anyhow::anyhow; +use rgb::RGB8; + +use super::property::Properties; + +/// A color that can be used to define the corner and/or edge of a Wang tile. +#[derive(PartialEq, Debug)] +pub struct WangColor { + /// The name of this color. + pub name: String, + /// The color in #RRGGBB format (example: #c17d11). + pub color: RGB8, + /// The local tile ID of the tile representing this color. + pub tile: u32, + /// The relative probability that this color is chosen over others in case of multiple options. + /// (defaults to 0) + pub probability: f64, + + // Can contain at most one: + pub properties: Properties, +} + +impl WangColor { + pub fn from_xml(node: roxmltree::Node) -> anyhow::Result { + ensure_element!(node); + ensure_tag_name!(node, "wangcolor"); + + let color = { + let mut c = get_attribute!(node, "color")? + .trim_start_matches("#") + .chars(); + if let Some(color) = read_color::rgb(&mut c) { + Ok(RGB8 { + r: color[0], + g: color[1], + b: color[2], + }) + } else { + Err(anyhow!("could not parse color")) + } + }?; + + let probability: f64 = match node.attribute("probability") { + Some(p) => p.parse()?, + None => 0f64, + }; + + let properties: Properties = match node.children().find(|&c| c.has_tag_name("properties")) { + Some(node) => Properties::from_xml(node)?, + None => Properties::default(), + }; + + Ok(WangColor { + name: get_attribute!(node, "name")?.to_owned(), + color, + tile: get_attribute!(node, "tile")?.parse()?, + probability, + properties, + }) + } +} + +#[cfg(test)] +mod test_wang_color { + use crate::lowlevel::property::*; + use crate::lowlevel::wangset::WangColor; + use crate::parse_property_test; + use rgb::RGB8; + + #[test] + fn parse_from_xml() { + parse_property_test!( + WangColor, + r##""##, + WangColor { + name: "Desert".to_owned(), + color: RGB8 { r: 255, g: 0, b: 0 }, + tile: 29, + probability: 1.0, + properties: Properties::default() + } + ); + parse_property_test!( + WangColor, + r##""##, + WangColor { + name: "Brick".to_owned(), + color: RGB8 { r: 0, g: 255, b: 0 }, + tile: 9, + probability: 1.0, + properties: Properties::default() + } + ); + parse_property_test!( + WangColor, + r##""##, + WangColor { + name: "Cobblestone".to_owned(), + color: RGB8 { r: 0, g: 0, b: 255 }, + tile: 33, + probability: 0.8, + properties: Properties::default() + } + ); + } + + #[test] + fn parse_from_xml_probability_missing() { + parse_property_test!( + WangColor, + r##""##, + WangColor { + name: "Desert".to_owned(), + color: RGB8 { r: 255, g: 0, b: 0 }, + tile: 29, + probability: 0.0, + properties: Properties::default() + } + ); + } + + // TODO a test with properties set +} + +/// “The Wang ID, given by a comma-separated list of indexes +/// (starting from 1, because 0 means _unset_) +/// referring to the Wang colors in the Wang set in the following order: +/// top, top right, right, bottom right, bottom, bottom left, left, top left (since Tiled 1.5). +/// Before Tiled 1.5, the Wang ID was saved as a 32-bit unsigned integer +/// stored in the format 0xCECECECE (where each C is a corner color and each E is an edge color, +/// in reverse order).” +/// The values are the indexes of the wangcolors of this set +#[derive(PartialEq, Eq, Debug)] +pub struct WangId { + pub top: u8, + pub top_right: u8, + pub right: u8, + pub bottom_right: u8, + pub bottom: u8, + pub bottom_left: u8, + pub left: u8, + pub top_left: u8, +} + +impl WangId { + pub fn from_string(wang_id_string: &str) -> anyhow::Result { + let ids: Vec<&str> = wang_id_string.split(",").collect(); + if ids.len() != 8 { + return Err(anyhow!("wrong number of elements in wang id")); + } + + Ok(WangId { + top: ids[0].parse()?, + top_right: ids[1].parse()?, + right: ids[2].parse()?, + bottom_right: ids[3].parse()?, + bottom: ids[4].parse()?, + bottom_left: ids[5].parse()?, + left: ids[6].parse()?, + top_left: ids[7].parse()?, + }) + } +} + +#[macro_export] +macro_rules! quick_wangid { + ( $top: expr, $top_right: expr, $right: expr, $bottom_right: expr, $bottom: expr, $bottom_left: expr, $left: expr, $top_left: expr) => { + WangId { + top: $top, + top_right: $top_right, + right: $right, + bottom_right: $bottom_right, + bottom: $bottom, + bottom_left: $bottom_left, + left: $left, + top_left: $top_left, + } + }; +} + +#[cfg(test)] +mod test_wang_id { + use crate::lowlevel::wangset::WangId; + use crate::quick_wangid; + + #[test] + fn parse_from_string() { + assert_eq!( + WangId::from_string("0,1,0,2,0,1,0,1").unwrap(), + WangId { + top: 0, + top_right: 1, + right: 0, + bottom_right: 2, + bottom: 0, + bottom_left: 1, + left: 0, + top_left: 1, + } + ); + assert_eq!( + WangId::from_string("0,1,0,2,0,2,0,1").unwrap(), + quick_wangid!(0, 1, 0, 2, 0, 2, 0, 1) + ); + assert_eq!( + WangId::from_string("0,2,0,2,0,1,0,1").unwrap(), + quick_wangid!(0, 2, 0, 2, 0, 1, 0, 1) + ); + assert_eq!( + WangId::from_string("0,3,0,3,0,1,0,1").unwrap(), + quick_wangid!(0, 3, 0, 3, 0, 1, 0, 1) + ); + assert_eq!( + WangId::from_string("0,1,0,3,0,3,0,3").unwrap(), + quick_wangid!(0, 1, 0, 3, 0, 3, 0, 3) + ); + assert_eq!( + WangId::from_string("0,1,0,1,0,1,0,1").unwrap(), + quick_wangid!(0, 1, 0, 1, 0, 1, 0, 1) + ); + } +} + +///Defines a Wang tile, by referring to a tile in the tileset and associating it with a certain Wang ID. +#[derive(PartialEq, Debug)] +pub struct WangTile { + /// The tile ID. + pub tile_id: u32, + /// The Wang ID + pub wangid: WangId, + // /// Whether the tile is flipped horizontally (removed in Tiled 1.5). + // pub hflip: bool, + // /// Whether the tile is flipped vertically (removed in Tiled 1.5). + // pub vflip: bool, + // /// Whether the tile is flipped on its diagonal (removed in Tiled 1.5). + // pub dflip: bool, +} + +impl WangTile { + pub fn from_xml(node: roxmltree::Node) -> anyhow::Result { + ensure_element!(node); + ensure_tag_name!(node, "wangtile"); + + Ok(WangTile { + tile_id: get_attribute!(node, "tileid")?.parse()?, + wangid: WangId::from_string(get_attribute!(node, "wangid")?)?, + }) + } +} + +#[cfg(test)] +mod test_wang_tile { + use crate::lowlevel::wangset::{WangId, WangTile}; + use crate::parse_property_test; + use crate::quick_wangid; + + #[test] + fn parse_from_xml() { + parse_property_test!( + WangTile, + r##""##, + WangTile { + tile_id: 2, + wangid: quick_wangid!(0, 1, 0, 1, 0, 2, 0, 1) + } + ); + } +} + +/// Defines a list of corner colors and a list of edge colors, +/// and any number of Wang tiles using these colors. +#[derive(PartialEq, Debug)] +pub struct WangSet { + ///The name of the Wang set. + pub name: String, + /// The tile ID of the tile representing this Wang set. + pub tile: u32, + + // Can contain at most one: + pub properties: Properties, + + /// Can contain up to 255: (since Tiled 1.5) + pub wangcolor: Vec, + + /// Can contain any number: + pub wangtiles: Vec, +} + +impl WangSet { + pub fn from_xml(node: roxmltree::Node) -> anyhow::Result { + ensure_element!(node); + ensure_tag_name!(node, "wangset"); + + let properties = match node.children().find(|&c| c.has_tag_name("properties")) { + Some(node) => Properties::from_xml(node)?, + None => Properties::default(), + }; + + let mut wangcolor = Vec::new(); + for wangcolor_node in node.children().filter(|&c| c.has_tag_name("wangcolor")) { + wangcolor.push(WangColor::from_xml(wangcolor_node)?); + } + + let mut wangtiles = Vec::new(); + for wangtile_node in node.children().filter(|&c| c.has_tag_name("wangtile")) { + wangtiles.push(WangTile::from_xml(wangtile_node)?); + } + + Ok(WangSet { + name: get_attribute!(node, "name")?.to_owned(), + tile: get_attribute!(node, "tile")?.parse()?, + properties, + wangcolor, + wangtiles, + }) + } +} + +#[cfg(test)] +mod test_wang_set { + use crate::lowlevel::wangset::*; + use crate::parse_property_test; + use crate::quick_wangid; + + macro_rules! wang_tile { + ($tile_id:expr, $wang_id:expr) => { + WangTile { + tile_id: $tile_id, + wangid: $wang_id, + } + }; + } + + #[test] + fn parse_desert_example_xml() { + parse_property_test!( + WangSet, + r##" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + "##, + WangSet { + name: "Desert".to_owned(), + tile: 5, + properties: Properties::default(), + wangcolor: vec![ + WangColor { + name: "Desert".to_owned(), + color: RGB8 { r: 255, g: 0, b: 0 }, + tile: 29, + probability: 1.0, + properties: Properties::default() + }, + WangColor { + name: "Brick".to_owned(), + color: RGB8 { r: 0, g: 255, b: 0 }, + tile: 9, + probability: 1.0, + properties: Properties::default() + }, + WangColor { + name: "Cobblestone".to_owned(), + color: RGB8 { r: 0, g: 0, b: 255 }, + tile: 33, + probability: 1.0, + properties: Properties::default() + }, + WangColor { + name: "Dirt".to_owned(), + color: RGB8 { + r: 255, + g: 119, + b: 0 + }, + tile: 14, + probability: 1.0, + properties: Properties::default() + } + ], + wangtiles: vec![ + wang_tile!(0, quick_wangid!(0, 1, 0, 2, 0, 1, 0, 1)), + wang_tile!(1, quick_wangid!(0, 1, 0, 2, 0, 2, 0, 1)), + wang_tile!(2, quick_wangid!(0, 1, 0, 1, 0, 2, 0, 1)), + wang_tile!(3, quick_wangid!(0, 4, 0, 1, 0, 4, 0, 4)), + wang_tile!(4, quick_wangid!(0, 4, 0, 4, 0, 1, 0, 4)), + wang_tile!(5, quick_wangid!(0, 1, 0, 4, 0, 1, 0, 1)), + wang_tile!(6, quick_wangid!(0, 1, 0, 4, 0, 4, 0, 1)), + wang_tile!(7, quick_wangid!(0, 1, 0, 1, 0, 4, 0, 1)), + wang_tile!(8, quick_wangid!(0, 2, 0, 2, 0, 1, 0, 1)), + wang_tile!(9, quick_wangid!(0, 2, 0, 2, 0, 2, 0, 2)), + wang_tile!(10, quick_wangid!(0, 1, 0, 1, 0, 2, 0, 2)), + wang_tile!(11, quick_wangid!(0, 1, 0, 4, 0, 4, 0, 4)), + wang_tile!(12, quick_wangid!(0, 4, 0, 4, 0, 4, 0, 1)), + wang_tile!(13, quick_wangid!(0, 4, 0, 4, 0, 1, 0, 1)), + wang_tile!(14, quick_wangid!(0, 4, 0, 4, 0, 4, 0, 4)), + wang_tile!(15, quick_wangid!(0, 1, 0, 1, 0, 4, 0, 4)), + wang_tile!(16, quick_wangid!(0, 2, 0, 1, 0, 1, 0, 1)), + wang_tile!(17, quick_wangid!(0, 2, 0, 1, 0, 1, 0, 2)), + wang_tile!(18, quick_wangid!(0, 1, 0, 1, 0, 1, 0, 2)), + wang_tile!(19, quick_wangid!(0, 2, 0, 1, 0, 2, 0, 2)), + wang_tile!(20, quick_wangid!(0, 2, 0, 2, 0, 1, 0, 2)), + wang_tile!(21, quick_wangid!(0, 4, 0, 1, 0, 1, 0, 1)), + wang_tile!(22, quick_wangid!(0, 4, 0, 1, 0, 1, 0, 4)), + wang_tile!(23, quick_wangid!(0, 1, 0, 1, 0, 1, 0, 4)), + wang_tile!(24, quick_wangid!(0, 1, 0, 3, 0, 1, 0, 1)), + wang_tile!(25, quick_wangid!(0, 1, 0, 3, 0, 3, 0, 1)), + wang_tile!(26, quick_wangid!(0, 1, 0, 1, 0, 3, 0, 1)), + wang_tile!(27, quick_wangid!(0, 1, 0, 2, 0, 2, 0, 2)), + wang_tile!(28, quick_wangid!(0, 2, 0, 2, 0, 2, 0, 1)), + wang_tile!(29, quick_wangid!(0, 1, 0, 1, 0, 1, 0, 1)), + wang_tile!(30, quick_wangid!(0, 1, 0, 1, 0, 1, 0, 1)), + wang_tile!(31, quick_wangid!(0, 1, 0, 1, 0, 1, 0, 1)), + wang_tile!(32, quick_wangid!(0, 3, 0, 3, 0, 1, 0, 1)), + wang_tile!(33, quick_wangid!(0, 3, 0, 3, 0, 3, 0, 3)), + wang_tile!(34, quick_wangid!(0, 1, 0, 1, 0, 3, 0, 3)), + wang_tile!(35, quick_wangid!(0, 3, 0, 1, 0, 3, 0, 3)), + wang_tile!(36, quick_wangid!(0, 3, 0, 3, 0, 1, 0, 3)), + wang_tile!(37, quick_wangid!(0, 1, 0, 1, 0, 1, 0, 1)), + wang_tile!(38, quick_wangid!(0, 1, 0, 1, 0, 1, 0, 1)), + wang_tile!(39, quick_wangid!(0, 1, 0, 1, 0, 1, 0, 1)), + wang_tile!(40, quick_wangid!(0, 3, 0, 1, 0, 1, 0, 1)), + wang_tile!(41, quick_wangid!(0, 3, 0, 1, 0, 1, 0, 3)), + wang_tile!(42, quick_wangid!(0, 1, 0, 1, 0, 1, 0, 3)), + wang_tile!(43, quick_wangid!(0, 1, 0, 3, 0, 3, 0, 3)), + wang_tile!(44, quick_wangid!(0, 3, 0, 3, 0, 3, 0, 1)), + wang_tile!(45, quick_wangid!(0, 1, 0, 1, 0, 1, 0, 1)), + wang_tile!(46, quick_wangid!(0, 1, 0, 1, 0, 1, 0, 1)), + wang_tile!(47, quick_wangid!(0, 1, 0, 1, 0, 1, 0, 1)), + ] + } + ); + } +} -- cgit v1.2.3-60-g2f50