summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLawnCable <git@lawncable.net>2022-04-15 08:37:04 +0200
committerLawnCable <git@lawncable.net>2022-04-15 08:37:04 +0200
commitd775eb1e9c6be67e5c07cabde79145b4ed3136e1 (patch)
treed51c17d4f55690e84e27fd8dcbe4f7e0110ad450
parent3e3f9d016e09493a37ae5935cecddb5a315350a6 (diff)
downloadfast-tiled.rs-d775eb1e9c6be67e5c07cabde79145b4ed3136e1.tar.gz
fast-tiled.rs-d775eb1e9c6be67e5c07cabde79145b4ed3136e1.tar.bz2
fast-tiled.rs-d775eb1e9c6be67e5c07cabde79145b4ed3136e1.tar.xz
fast-tiled.rs-d775eb1e9c6be67e5c07cabde79145b4ed3136e1.zip
improvements, wangset and images
alse parsing for may tileset related types
-rw-r--r--readme.md21
-rw-r--r--src/lowlevel/data.rs7
-rw-r--r--src/lowlevel/image.rs139
-rw-r--r--src/lowlevel/mod.rs2
-rw-r--r--src/lowlevel/property.rs17
-rw-r--r--src/lowlevel/tileset.rs441
-rw-r--r--src/lowlevel/wangset.rs484
7 files changed, 1035 insertions, 76 deletions
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
| `<editorsettings>` | - | - | - |
| `.<chunksize>` | - | - | - |
| `.<export>` | - | - | - |
-| `<tileset>` | partial | - | - |
+| `<tileset>` | complete | - | - |
| `.<tileoffset>` | complete | complete | complete |
| `.<grid>` | complete | complete | complete |
-| `.<image>` | partial | - | - |
+| `.<image>` | partial | partial | partial |
+| `..image in <data>` | partial | - | - |
| `.<terraintypes>` | complete | - | - |
| `..<terrain>` | complete | complete | - |
| `.<transformations>` | complete | complete | complete |
-| `.<tile>` | partial | - | - |
-| `..<animation>` | complete | - | - |
-| `.<frame>` | complete | - | - |
+| `.<tile>` | partial (5/6) | partial (5/6) | partial (3/6) |
+| `..<animation>` | complete | complete | complete |
+| `.<frame>` | complete | complete | complete |
| `.<wangsets>` | partial | - | - |
-| `..<wangset>` | partial | - | - |
-| `...<wangcolor>` | complete | - | - |
-| `...<wangtile>` | partial | - | - |
-| `....Wang ID` | - | - | - |
+| `..<wangset>` | complete | complete | complete |
+| `...<wangcolor>` | complete | complete | complete |
+| `...<wangtile>` | complete | complete | complete |
+| `....Wang ID` | complete | complete | complete |
| `<layer>` | - | - | - |
| `.<data>` | - | - | - |
| `.<chunk>` | - | - | - |
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 <tile> 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<usize>,
}
+
+impl Image {
+ pub fn from_xml(node: roxmltree::Node) -> anyhow::Result<Image> {
+ ensure_element!(node);
+ ensure_tag_name!(node, "image");
+
+ let width: Option<usize> = match node.attribute("width") {
+ Some(s) => Some(s.parse()?),
+ None => None,
+ };
+
+ let height: Option<usize> = match node.attribute("height") {
+ Some(s) => Some(s.parse()?),
+ None => None,
+ };
+
+ let transparent_color: Option<RGB8> = 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="beach_tileset.png" width="576" height="416"/>"##,
+ 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="perspective_walls.png"/>"##,
+ 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="sewer_tileset.png" trans="ff00ff" width="192" height="217"/>"##,
+ 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="sewer_tileset.png" trans="#ff00ff" width="192" height="217"/>"##,
+ 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 name="door" value="true"/>"##,
+ 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<ObjectAlignment> {
+ 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 <tileset>.
tileid: u32,
@@ -31,7 +55,80 @@ pub struct Frame {
duration: u32,
}
+impl Frame {
+ pub fn from_xml(node: roxmltree::Node) -> anyhow::Result<Frame> {
+ 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<Vec<Frame>> {
+ ensure_element!(node);
+ ensure_tag_name!(node, "animation");
+ let mut frames: Vec<Frame> = 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"/>"##,
+ Frame {
+ tileid: 37,
+ duration: 1000
+ }
+ );
+ }
+
+ #[test]
+ fn animation() {
+ let source = r##"<animation>
+ <frame tileid="148" duration="1000"/>
+ <frame tileid="157" duration="1000"/>
+ <frame tileid="166" duration="1000"/>
+ <frame tileid="175" duration="1000"/>
+ </animation>"##;
+ 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: <image> (since 0.9), <objectgroup>, <animation>
- pub image: Option<super::image::ImageSource>,
+ pub image: Option<super::image::Image>,
+ // TODO <objectgroup>,
/// 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<Vec<Frame>>,
}
+impl Tile {
+ pub fn from_xml(node: roxmltree::Node) -> anyhow::Result<Tile> {
+ 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<Vec<Frame>> =
+ 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 <objectgroup>,
+
+ 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">
+ <properties>
+ <property name="door" value="true"/>
+ </properties>
+ </tile>"##,
+ 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">
+ <animation>
+ <frame tileid="148" duration="1000"/>
+ <frame tileid="157" duration="1000"/>
+ <frame tileid="166" duration="1000"/>
+ <frame tileid="175" duration="1000"/>
+ </animation>
+ </tile>"##,
+ 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: <properties>
- 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: <properties>
- pub properties: Properties,
-
- /// Can contain up to 255: <wangcolor> (since Tiled 1.5)
- pub wangcolor: Vec<WangColor>,
-
- /// Can contain any number: <wangtile>
- pub wangtiles: Vec<WangTile>,
-}
/// 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<TileSet> {
+ 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<Tile> = 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<Grid> = 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: <properties>
+ pub properties: Properties,
+}
+
+impl WangColor {
+ pub fn from_xml(node: roxmltree::Node) -> anyhow::Result<WangColor> {
+ 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" color="#ff0000" tile="29" probability="1"/>"##,
+ 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" color="#00ff00" tile="9" probability="1"/>"##,
+ 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" color="#0000ff" tile="33" probability="0.8"/>"##,
+ 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" color="#ff0000" tile="29" />"##,
+ 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<WangId> {
+ 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<WangTile> {
+ 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 tileid="2" wangid="0,1,0,1,0,2,0,1"/>"##,
+ 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: <properties>
+ pub properties: Properties,
+
+ /// Can contain up to 255: <wangcolor> (since Tiled 1.5)
+ pub wangcolor: Vec<WangColor>,
+
+ /// Can contain any number: <wangtile>
+ pub wangtiles: Vec<WangTile>,
+}
+
+impl WangSet {
+ pub fn from_xml(node: roxmltree::Node) -> anyhow::Result<WangSet> {
+ 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" type="corner" tile="5">
+ <wangcolor name="Desert" color="#ff0000" tile="29" probability="1"/>
+ <wangcolor name="Brick" color="#00ff00" tile="9" probability="1"/>
+ <wangcolor name="Cobblestone" color="#0000ff" tile="33" probability="1"/>
+ <wangcolor name="Dirt" color="#ff7700" tile="14" probability="1"/>
+ <wangtile tileid="0" wangid="0,1,0,2,0,1,0,1"/>
+ <wangtile tileid="1" wangid="0,1,0,2,0,2,0,1"/>
+ <wangtile tileid="2" wangid="0,1,0,1,0,2,0,1"/>
+ <wangtile tileid="3" wangid="0,4,0,1,0,4,0,4"/>
+ <wangtile tileid="4" wangid="0,4,0,4,0,1,0,4"/>
+ <wangtile tileid="5" wangid="0,1,0,4,0,1,0,1"/>
+ <wangtile tileid="6" wangid="0,1,0,4,0,4,0,1"/>
+ <wangtile tileid="7" wangid="0,1,0,1,0,4,0,1"/>
+ <wangtile tileid="8" wangid="0,2,0,2,0,1,0,1"/>
+ <wangtile tileid="9" wangid="0,2,0,2,0,2,0,2"/>
+ <wangtile tileid="10" wangid="0,1,0,1,0,2,0,2"/>
+ <wangtile tileid="11" wangid="0,1,0,4,0,4,0,4"/>
+ <wangtile tileid="12" wangid="0,4,0,4,0,4,0,1"/>
+ <wangtile tileid="13" wangid="0,4,0,4,0,1,0,1"/>
+ <wangtile tileid="14" wangid="0,4,0,4,0,4,0,4"/>
+ <wangtile tileid="15" wangid="0,1,0,1,0,4,0,4"/>
+ <wangtile tileid="16" wangid="0,2,0,1,0,1,0,1"/>
+ <wangtile tileid="17" wangid="0,2,0,1,0,1,0,2"/>
+ <wangtile tileid="18" wangid="0,1,0,1,0,1,0,2"/>
+ <wangtile tileid="19" wangid="0,2,0,1,0,2,0,2"/>
+ <wangtile tileid="20" wangid="0,2,0,2,0,1,0,2"/>
+ <wangtile tileid="21" wangid="0,4,0,1,0,1,0,1"/>
+ <wangtile tileid="22" wangid="0,4,0,1,0,1,0,4"/>
+ <wangtile tileid="23" wangid="0,1,0,1,0,1,0,4"/>
+ <wangtile tileid="24" wangid="0,1,0,3,0,1,0,1"/>
+ <wangtile tileid="25" wangid="0,1,0,3,0,3,0,1"/>
+ <wangtile tileid="26" wangid="0,1,0,1,0,3,0,1"/>
+ <wangtile tileid="27" wangid="0,1,0,2,0,2,0,2"/>
+ <wangtile tileid="28" wangid="0,2,0,2,0,2,0,1"/>
+ <wangtile tileid="29" wangid="0,1,0,1,0,1,0,1"/>
+ <wangtile tileid="30" wangid="0,1,0,1,0,1,0,1"/>
+ <wangtile tileid="31" wangid="0,1,0,1,0,1,0,1"/>
+ <wangtile tileid="32" wangid="0,3,0,3,0,1,0,1"/>
+ <wangtile tileid="33" wangid="0,3,0,3,0,3,0,3"/>
+ <wangtile tileid="34" wangid="0,1,0,1,0,3,0,3"/>
+ <wangtile tileid="35" wangid="0,3,0,1,0,3,0,3"/>
+ <wangtile tileid="36" wangid="0,3,0,3,0,1,0,3"/>
+ <wangtile tileid="37" wangid="0,1,0,1,0,1,0,1"/>
+ <wangtile tileid="38" wangid="0,1,0,1,0,1,0,1"/>
+ <wangtile tileid="39" wangid="0,1,0,1,0,1,0,1"/>
+ <wangtile tileid="40" wangid="0,3,0,1,0,1,0,1"/>
+ <wangtile tileid="41" wangid="0,3,0,1,0,1,0,3"/>
+ <wangtile tileid="42" wangid="0,1,0,1,0,1,0,3"/>
+ <wangtile tileid="43" wangid="0,1,0,3,0,3,0,3"/>
+ <wangtile tileid="44" wangid="0,3,0,3,0,3,0,1"/>
+ <wangtile tileid="45" wangid="0,1,0,1,0,1,0,1"/>
+ <wangtile tileid="46" wangid="0,1,0,1,0,1,0,1"/>
+ <wangtile tileid="47" wangid="0,1,0,1,0,1,0,1"/>
+ </wangset>"##,
+ 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)),
+ ]
+ }
+ );
+ }
+}