summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLawnCable <git@lawncable.net>2022-04-15 08:37:02 +0200
committerLawnCable <git@lawncable.net>2022-04-15 08:37:02 +0200
commit3e3f9d016e09493a37ae5935cecddb5a315350a6 (patch)
treeb4627f1a0b530fbf781f61a74239ef0ac3e28730
parentec09c0ae43799846a68a4e100a68353730edfed1 (diff)
downloadfast-tiled.rs-3e3f9d016e09493a37ae5935cecddb5a315350a6.tar.gz
fast-tiled.rs-3e3f9d016e09493a37ae5935cecddb5a315350a6.tar.bz2
fast-tiled.rs-3e3f9d016e09493a37ae5935cecddb5a315350a6.tar.xz
fast-tiled.rs-3e3f9d016e09493a37ae5935cecddb5a315350a6.zip
more progress
-rw-r--r--readme.md51
-rw-r--r--src/lib.rs4
-rw-r--r--src/lowlevel/data.rs28
-rw-r--r--src/lowlevel/image.rs28
-rw-r--r--src/lowlevel/layer.rs17
-rw-r--r--src/lowlevel/macros.rs38
-rw-r--r--src/lowlevel/map.rs99
-rw-r--r--src/lowlevel/mod.rs10
-rw-r--r--src/lowlevel/property.rs278
-rw-r--r--src/lowlevel/tileset.rs443
-rw-r--r--src/main.rs34
11 files changed, 950 insertions, 80 deletions
diff --git a/readme.md b/readme.md
index 6453a5e..0eacb82 100644
--- a/readme.md
+++ b/readme.md
@@ -17,4 +17,53 @@ After that is done there could be more high level methods/struct that provide mo
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
+- parsing from and serializing back to the tiled JSON based formats
+
+## Thanks to
+
+- https://docs.rs/tmx/0.3.1/ - Inspiration for data types.
+
+## Notes:
+
+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 |
+| -------------------- | ------------------- | -------------- | --------------- |
+| `<map>` | partial | - | - |
+| `<editorsettings>` | - | - | - |
+| `.<chunksize>` | - | - | - |
+| `.<export>` | - | - | - |
+| `<tileset>` | partial | - | - |
+| `.<tileoffset>` | complete | complete | complete |
+| `.<grid>` | complete | complete | complete |
+| `.<image>` | partial | - | - |
+| `.<terraintypes>` | complete | - | - |
+| `..<terrain>` | complete | complete | - |
+| `.<transformations>` | complete | complete | complete |
+| `.<tile>` | partial | - | - |
+| `..<animation>` | complete | - | - |
+| `.<frame>` | complete | - | - |
+| `.<wangsets>` | partial | - | - |
+| `..<wangset>` | partial | - | - |
+| `...<wangcolor>` | complete | - | - |
+| `...<wangtile>` | partial | - | - |
+| `....Wang ID` | - | - | - |
+| `<layer>` | - | - | - |
+| `.<data>` | - | - | - |
+| `.<chunk>` | - | - | - |
+| `.<tile>` | - | - | - |
+| `<objectgroup>` | - | - | - |
+| `.<object>` | - | - | - |
+| `.<ellipse>` | - | - | - |
+| `.<point>` | - | - | - |
+| `.<polygon>` | - | - | - |
+| `.<polyline>` | - | - | - |
+| `.<text>` | - | - | - |
+| `<imagelayer>` | - | - | - |
+| `<group>` | - | - | - |
+| `<properties>` | complete | yes | yes |
+| `.<property>` | complete | yes | yes |
+| `<template>` | - | - | - |
diff --git a/src/lib.rs b/src/lib.rs
index 3c9002e..df5e266 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,3 +1 @@
-
-
-pub mod lowlevel; \ No newline at end of file
+pub mod lowlevel;
diff --git a/src/lowlevel/data.rs b/src/lowlevel/data.rs
new file mode 100644
index 0000000..7b64a12
--- /dev/null
+++ b/src/lowlevel/data.rs
@@ -0,0 +1,28 @@
+struct LayerData {}
+
+/// The encoding used to encode the tile layer data. When used, it can be “base64” and “csv” at the moment.
+pub enum Encoding {
+ Base64,
+ CSV,
+}
+
+/// 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 {
+ None,
+ Gzip,
+ Zlib,
+ Zstd,
+}
+
+pub struct EncodedData {
+ pub encoding: Encoding,
+ pub compression: Compression,
+ pub data: String,
+}
+
+impl EncodedData {
+ // fn decode(self) -> &[u8] {
+
+ // }
+}
diff --git a/src/lowlevel/image.rs b/src/lowlevel/image.rs
new file mode 100644
index 0000000..5374b8c
--- /dev/null
+++ b/src/lowlevel/image.rs
@@ -0,0 +1,28 @@
+use rgb::RGB8;
+
+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.
+ source: String,
+ },
+
+ /// Embedded images with a data child element
+ ///
+ /// Note that it is not currently possible to use Tiled to create maps with embedded image data, even though the TMX format supports this
+ Embedded {
+ /// . Valid values are file extensions like png, gif, jpg, bmp, etc.
+ format: String,
+ data: super::data::EncodedData,
+ },
+}
+
+pub struct Image {
+ pub source: ImageSource,
+ /// Defines a specific color that is treated as transparent (example value: “#FF00FF” for magenta).
+ /// Including the “#” is optional and Tiled leaves it out for compatibility reasons. (optional)
+ pub transparent_color: Option<RGB8>,
+ /// The image width in pixels (optional, used for tile index correction when the image changes)
+ pub width: Option<usize>,
+ /// The image height in pixels (optional)
+ pub height: Option<usize>,
+}
diff --git a/src/lowlevel/layer.rs b/src/lowlevel/layer.rs
new file mode 100644
index 0000000..ebb6cae
--- /dev/null
+++ b/src/lowlevel/layer.rs
@@ -0,0 +1,17 @@
+pub struct LayerTile {
+ /// Global Tile Id
+ pub gid: u32,
+}
+
+pub struct LayerChunk {
+ pub x: u32,
+ pub y: u32,
+ pub width: u32,
+ pub height: u32,
+ pub data: Vec<LayerTile>,
+}
+
+pub enum LayerData {
+ Tiles(Vec<LayerTile>),
+ Chunks(Vec<LayerChunk>),
+}
diff --git a/src/lowlevel/macros.rs b/src/lowlevel/macros.rs
new file mode 100644
index 0000000..7568161
--- /dev/null
+++ b/src/lowlevel/macros.rs
@@ -0,0 +1,38 @@
+use anyhow::anyhow;
+
+#[macro_export]
+macro_rules! parse_property_test {
+ ($type:ident, $input:expr, $expected_output:expr) => {{
+ let doc = roxmltree::Document::parse($input).unwrap();
+ let elem = doc.root_element();
+ assert_eq!($type::from_xml(elem).unwrap(), $expected_output);
+ }};
+}
+
+#[macro_export]
+macro_rules! get_attribute {
+ ($node:expr, $attribute_name:expr) => {{
+ let node: &roxmltree::Node = &$node;
+ let attribute_name: &str = $attribute_name;
+ node.attribute(attribute_name)
+ .ok_or(anyhow!("{} attribute missing", attribute_name))
+ }};
+}
+
+#[macro_export]
+macro_rules! ensure_element {
+ ($node:expr) => {
+ if $node.node_type() != roxmltree::NodeType::Element {
+ return Err(anyhow!("xml -> node is not an element"));
+ }
+ };
+}
+
+#[macro_export]
+macro_rules! ensure_tag_name {
+ ($node:expr, $tag_name:expr) => {
+ if !$node.has_tag_name($tag_name) {
+ return Err(anyhow!("xml -> node is not an '{}' element ", $tag_name));
+ }
+ };
+}
diff --git a/src/lowlevel/map.rs b/src/lowlevel/map.rs
index c2a33cf..63cc9e1 100644
--- a/src/lowlevel/map.rs
+++ b/src/lowlevel/map.rs
@@ -8,18 +8,68 @@ use rgb::RGBA;
pub enum MapOrientation {
Orthogonal,
Isometric,
- Staggered,
- Hexagonal,
+ Staggered {
+ /// For staggered and hexagonal maps, determines whether the “even” or “odd” indexes along the staggered axis are shifted. (since 0.11)
+ stagger_axis: StaggerAxis,
+ /// For staggered and hexagonal maps, determines which axis (“x” or “y”) is staggered. (since 0.11)
+ stagger_index: StaggerIndex,
+ },
+ Hexagonal {
+ /// Only for hexagonal maps. Determines the width or height (depending on the staggered axis) of the tile’s edge, in pixels.
+ hexside_length: i32,
+ /// For staggered and hexagonal maps, determines whether the “even” or “odd” indexes along the staggered axis are shifted. (since 0.11)
+ stagger_axis: StaggerAxis,
+ /// For staggered and hexagonal maps, determines which axis (“x” or “y”) is staggered. (since 0.11)
+ stagger_index: StaggerIndex,
+ },
+ // thanks https://docs.rs/tmx/0.3.1/tmx/map/enum.Orientation.html for this format idea.
}
impl MapOrientation {
- pub fn from_string(string: &str) -> anyhow::Result<MapOrientation> {
- match string {
+ pub fn from_xml_map_node(node: &roxmltree::Node) -> anyhow::Result<MapOrientation> {
+ let orientation = node
+ .attribute("orientation")
+ .ok_or(anyhow!("map orientation missing"))?;
+
+ match orientation {
"orthogonal" => Ok(MapOrientation::Orthogonal),
"isometric" => Ok(MapOrientation::Isometric),
- "staggered" => Ok(MapOrientation::Staggered),
- "hexagonal" => Ok(MapOrientation::Hexagonal),
- _ => Err(anyhow!("Unknown MapOrientation: {}", string)),
+ "staggered" => {
+ let stagger_axis = StaggerAxis::from_string(node.attribute("staggeraxis").ok_or(
+ anyhow!("map.staggeraxis missing, it is required for orientation=staggered"),
+ )?)?;
+ let stagger_index =
+ StaggerIndex::from_string(node.attribute("staggerindex").ok_or(anyhow!(
+ "map.staggerindex missing, it is required for orientation=staggered"
+ ))?)?;
+
+ Ok(MapOrientation::Staggered {
+ stagger_axis,
+ stagger_index,
+ })
+ }
+ "hexagonal" => {
+ let stagger_axis = StaggerAxis::from_string(node.attribute("staggeraxis").ok_or(
+ anyhow!("map.staggeraxis missing, it is required for orientation=hexagonal"),
+ )?)?;
+ let stagger_index =
+ StaggerIndex::from_string(node.attribute("staggerindex").ok_or(anyhow!(
+ "map.staggerindex missing, it is required for orientation=hexagonal"
+ ))?)?;
+ let hexside_length: i32 = node
+ .attribute("hexsidelength")
+ .ok_or(anyhow!(
+ "map.hexsidelength missing, it is required for orientation=hexagonal"
+ ))?
+ .parse()?;
+
+ Ok(MapOrientation::Hexagonal {
+ stagger_axis,
+ stagger_index,
+ hexside_length,
+ })
+ }
+ _ => Err(anyhow!("Unknown MapOrientation: {}", orientation)),
}
}
@@ -27,8 +77,8 @@ impl MapOrientation {
match self {
MapOrientation::Orthogonal => "orthogonal".to_owned(),
MapOrientation::Isometric => "isometric".to_owned(),
- MapOrientation::Staggered => "staggered".to_owned(),
- MapOrientation::Hexagonal => "hexagonal".to_owned(),
+ MapOrientation::Staggered { .. } => "staggered".to_owned(),
+ MapOrientation::Hexagonal { .. } => "hexagonal".to_owned(),
}
}
}
@@ -121,16 +171,16 @@ pub struct Map {
pub version: String,
/// The Tiled version used to save the file (since Tiled 1.0.1). May be a date (for snapshot builds). (optional)
- pub tiledversion: Option<String>,
+ pub tiled_version: Option<String>,
/// Map orientation. Tiled supports “orthogonal”, “isometric”, “staggered” and “hexagonal” (since 0.11).
pub orientation: MapOrientation,
/// The order in which tiles on tile layers are rendered.
- pub renderorder: MapRenderOrder,
+ pub render_order: MapRenderOrder,
/// The compression level to use for tile layer data (defaults to -1, which means to use the algorithm default).
- pub compressionlevel: Option<isize>, // TODO seems optional, please validate
+ pub compression_level: Option<isize>, // TODO seems optional, please validate
// The map width in tiles.
pub width: usize,
@@ -138,31 +188,28 @@ pub struct Map {
pub height: usize,
/// The width of a tile.
- pub tilewidth: usize,
+ pub tile_width: usize,
/// The height of a tile.
- pub tileheight: usize,
- /// Only for hexagonal maps. Determines the width or height (depending on the staggered axis) of the tile’s edge, in pixels.
- pub hexsidelength: Option<usize>,
-
- /// For staggered and hexagonal maps, determines which axis (“x” or “y”) is staggered. (since 0.11)
- pub staggeraxis: Option<StaggerAxis>,
- /// For staggered and hexagonal maps, determines whether the “even” or “odd” indexes along the staggered axis are shifted. (since 0.11)
- pub staggerindex: Option<StaggerIndex>,
+ pub tile_height: usize,
/// The background color of the map. (optional, may include alpha value since 0.15 in the form #AARRGGBB. Defaults to fully transparent.)
- pub backgroundcolor: Option<RGBA<u8>>,
+ pub background_color: Option<RGBA<u8>>,
/// Stores the next available ID for new layers. This number is stored to prevent reuse of the same ID after layers have been removed. (since 1.2) (defaults to the highest layer id in the file + 1)
- pub nextlayerid: usize, // TODO set default in funtions
+ pub next_layer_id: usize, // TODO set default in funtions
/// Stores the next available ID for new objects. This number is stored to prevent reuse of the same ID after objects have been removed. (since 0.11) (defaults to the highest object id in the file + 1)
- pub nextobjectid: usize,
+ pub next_object_id: usize,
/// Whether this map is infinite. An infinite map has no fixed size and can grow in all directions. Its layer data is stored in chunks. (0 for false, 1 for true, defaults to 0)
pub infinite: bool,
// xml child elements
- pub properties: Option<super::property::Properties>,
- // TODO Can contain any number: <tileset>, <layer>, <objectgroup>, <imagelayer>, <group> (since 1.0), <editorsettings> (since 1.3)
+ pub properties: super::property::Properties,
+
+ pub tilesets: Vec<super::tileset::TileSetElement>,
+
+ pub layer: Vec<super::layer::LayerData>,
+ // TODO Can contain any number: <objectgroup>, <imagelayer>, <group> (since 1.0), <editorsettings> (since 1.3)
}
impl Map {
diff --git a/src/lowlevel/mod.rs b/src/lowlevel/mod.rs
index 1a10f8d..34d0fb7 100644
--- a/src/lowlevel/mod.rs
+++ b/src/lowlevel/mod.rs
@@ -1,3 +1,13 @@
pub mod map;
pub mod property;
+
+pub mod tileset;
+
+pub mod image;
+
+pub mod data;
+
+pub mod layer;
+
+pub(crate) mod macros;
diff --git a/src/lowlevel/property.rs b/src/lowlevel/property.rs
index 26dcedd..bba693f 100644
--- a/src/lowlevel/property.rs
+++ b/src/lowlevel/property.rs
@@ -1,3 +1,4 @@
+use crate::{ensure_element, ensure_tag_name, get_attribute};
use anyhow::anyhow;
use rgb::RGBA8;
@@ -18,19 +19,14 @@ pub enum Property {
Object { name: String, value: usize },
}
+#[allow(dead_code)]
impl Property {
pub fn from_xml(node: roxmltree::Node) -> anyhow::Result<Property> {
- if node.node_type() != roxmltree::NodeType::Element {
- return Err(anyhow!("xml -> node is not an element"));
- }
+ ensure_element!(node);
+ ensure_tag_name!(node, "property");
- let name = node
- .attribute("name")
- .ok_or(anyhow!("property name missing"))?
- .to_owned();
- let property_type = node
- .attribute("type")
- .ok_or(anyhow!("property type missing"))?;
+ let name = get_attribute!(node, "name")?.to_owned();
+ let property_type = get_attribute!(node, "type")?;
// handle the case that 'string' value is stored in element content instead of value atribute
if property_type == "string" {
@@ -46,9 +42,7 @@ impl Property {
}
}
- let raw_value = node
- .attribute("value")
- .ok_or(anyhow!("property value missing"))?;
+ let raw_value = get_attribute!(node, "value")?;
match property_type {
"string" => Ok(Property::String {
@@ -106,6 +100,7 @@ impl Property {
}
}
+ #[allow(dead_code)]
pub fn value_to_string(self) -> String {
match self {
Property::String { value, .. } => value,
@@ -126,32 +121,124 @@ impl Property {
}
// Wraps any number of custom properties
-#[derive(Debug)]
+#[allow(dead_code)]
+#[derive(Debug, PartialEq)]
pub struct Properties {
pub properties: Vec<Property>,
}
+impl Default for Properties {
+ fn default() -> Self {
+ Properties { properties: vec![] }
+ }
+}
+
impl Properties {
- fn from_xml(node: roxmltree::Node) -> anyhow::Result<Properties> {
- Err(anyhow!("not implemented"))
+ pub fn from_xml(node: roxmltree::Node) -> anyhow::Result<Properties> {
+ let mut properties: Vec<Property> = Vec::new();
+ for property_node in node.children() {
+ if property_node.has_tag_name("property") {
+ properties.push(Property::from_xml(property_node)?)
+ }
+ }
+ Ok(Properties { properties })
}
}
#[cfg(test)]
-mod tests {
+mod parse_property {
use crate::lowlevel::property::Property;
use rgb::RGBA8;
#[test]
- fn parse_color_property() {
- // Find element by id
- let doc = roxmltree::Document::parse(
+ fn string_inside_attribute() {
+ crate::parse_property_test!(
+ Property,
+ r##"<property name="enemyText" type="string" value="hello world"/>"##,
+ Property::String {
+ name: "enemyText".to_owned(),
+ value: "hello world".to_owned()
+ }
+ );
+ }
+
+ #[test]
+ fn string_inside_element() {
+ crate::parse_property_test!(
+ Property,
+ r##"<property name="enemyText" type="string">hello world</property>"##,
+ Property::String {
+ name: "enemyText".to_owned(),
+ value: "hello world".to_owned()
+ }
+ );
+ }
+
+ #[test]
+ fn int() {
+ crate::parse_property_test!(
+ Property,
+ r##"<property name="enemyText" type="int" value="-8"/>"##,
+ Property::Int {
+ name: "enemyText".to_owned(),
+ value: -8
+ }
+ );
+ crate::parse_property_test!(
+ Property,
+ r##"<property name="enemyText" type="int" value="3453"/>"##,
+ Property::Int {
+ name: "enemyText".to_owned(),
+ value: 3453
+ }
+ );
+ }
+
+ #[test]
+ fn float() {
+ crate::parse_property_test!(
+ Property,
+ r##"<property name="ExperienceBoost" type="float" value="-8.8"/>"##,
+ Property::Float {
+ name: "ExperienceBoost".to_owned(),
+ value: -8.8
+ }
+ );
+ crate::parse_property_test!(
+ Property,
+ r##"<property name="ExperienceBoost" type="float" value="0.005"/>"##,
+ Property::Float {
+ name: "ExperienceBoost".to_owned(),
+ value: 0.005
+ }
+ );
+ }
+
+ #[test]
+ fn bool() {
+ crate::parse_property_test!(
+ Property,
+ r##"<property name="isNight" type="bool" value="true"/>"##,
+ Property::Bool {
+ name: "isNight".to_owned(),
+ value: true
+ }
+ );
+ crate::parse_property_test!(
+ Property,
+ r##"<property name="isNight" type="bool" value="false"/>"##,
+ Property::Bool {
+ name: "isNight".to_owned(),
+ value: false
+ }
+ );
+ }
+
+ #[test]
+ fn color() {
+ crate::parse_property_test!(
+ Property,
r##"<property name="enemyTint" type="color" value="#ffa33636"/>"##,
- )
- .unwrap();
- let elem = doc.root_element();
- assert_eq!(
- Property::from_xml(elem).unwrap(),
Property::Color {
name: "enemyTint".to_owned(),
value: RGBA8 {
@@ -161,41 +248,132 @@ mod tests {
a: 54
}
}
+ )
+ }
+
+ #[test]
+ fn file() {
+ crate::parse_property_test!(
+ Property,
+ r##"<property name="music" type="file" value="../music/cave.ogg"/>"##,
+ Property::File {
+ name: "music".to_owned(),
+ value: "../music/cave.ogg".to_owned()
+ }
);
}
#[test]
- fn parse_string_property_inside_attribute() {
- // Find element by id
- let doc = roxmltree::Document::parse(
- r##"<property name="enemyText" type="string" value="hello world"/>"##,
- )
- .unwrap();
- let elem = doc.root_element();
- assert_eq!(
- Property::from_xml(elem).unwrap(),
- Property::String {
- name: "enemyText".to_owned(),
- value: "hello world".to_owned()
+ fn object() {
+ crate::parse_property_test!(
+ Property,
+ r##"<property name="music" type="object" value="2583"/>"##,
+ Property::Object {
+ name: "music".to_owned(),
+ value: 2583
}
);
}
+}
+
+#[cfg(test)]
+mod parse_properties {
+ use crate::lowlevel::property::Properties;
+ use crate::lowlevel::property::Property;
+ use rgb::RGBA8;
#[test]
- fn parse_string_property_inside_element() {
- // Find element by id
- let doc = roxmltree::Document::parse(
- r##"<property name="enemyText" type="string">hello world</property>"##,
- )
- .unwrap();
- let elem = doc.root_element();
- println!("{:?}", elem.first_child());
- assert_eq!(
- Property::from_xml(elem).unwrap(),
- Property::String {
- name: "enemyText".to_owned(),
- value: "hello world".to_owned()
+ fn one_property() {
+ crate::parse_property_test!(
+ Properties,
+ r##"<properties>
+ <property name="enemyTint" type="color" value="#ffa33636"/>
+ </properties>"##,
+ Properties {
+ properties: vec![Property::Color {
+ name: "enemyTint".to_owned(),
+ value: RGBA8 {
+ r: 255,
+ g: 163,
+ b: 54,
+ a: 54
+ }
+ }]
+ }
+ );
+ }
+
+ #[test]
+ fn two_properties() {
+ crate::parse_property_test!(
+ Properties,
+ r##"<properties>
+ <property name="enemyTint" type="color" value="#ffa33636"/>
+ <property name="enemyDropChanceModifier" type="float" value="0.005"/>
+ </properties>"##,
+ Properties {
+ properties: vec![
+ Property::Color {
+ name: "enemyTint".to_owned(),
+ value: RGBA8 {
+ r: 255,
+ g: 163,
+ b: 54,
+ a: 54
+ }
+ },
+ Property::Float {
+ name: "enemyDropChanceModifier".to_owned(),
+ value: 0.005
+ }
+ ]
+ }
+ );
+ }
+
+ #[test]
+ fn two_properties_with_comments() {
+ crate::parse_property_test!(
+ Properties,
+ r##"<properties>
+ <!-- what color the enemy should have -->
+ <property name="enemyTint" type="color" value="#ffa33636"/>
+ <!-- a nother comment -->
+ <property name="enemyDropChanceModifier" type="float" value="0.005"/>
+ </properties>"##,
+ Properties {
+ properties: vec![
+ Property::Color {
+ name: "enemyTint".to_owned(),
+ value: RGBA8 {
+ r: 255,
+ g: 163,
+ b: 54,
+ a: 54
+ }
+ },
+ Property::Float {
+ name: "enemyDropChanceModifier".to_owned(),
+ value: 0.005
+ }
+ ]
}
);
}
+
+ #[test]
+ fn zero_properties() {
+ crate::parse_property_test!(
+ Properties,
+ r##"<properties></properties>"##,
+ Properties { properties: vec![] }
+ );
+ }
+
+ #[test]
+ fn default_value() {
+ let empty_properties: Properties = Default::default();
+
+ assert_eq!(empty_properties.properties.len(), 0)
+ }
}
diff --git a/src/lowlevel/tileset.rs b/src/lowlevel/tileset.rs
new file mode 100644
index 0000000..0e6dc87
--- /dev/null
+++ b/src/lowlevel/tileset.rs
@@ -0,0 +1,443 @@
+use crate::{ensure_element, ensure_tag_name, get_attribute};
+use anyhow::anyhow;
+use rgb::RGB8;
+
+use super::property::Properties;
+
+pub enum ObjectAlignment {
+ Unspecified,
+ TopLeft,
+ Top,
+ TopRight,
+ Left,
+ Center,
+ Right,
+ BottomLeft,
+ Bottom,
+ BottomRight,
+}
+
+impl Default for ObjectAlignment {
+ fn default() -> Self {
+ ObjectAlignment::Unspecified
+ }
+}
+
+pub struct Frame {
+ /// The local ID of a tile within the parent <tileset>.
+ tileid: u32,
+
+ // How long (in milliseconds) this frame should be displayed before advancing to the next frame.
+ duration: u32,
+}
+
+/// Define properties for a specific tile
+pub struct Tile {
+ /// The local tile ID within its tileset.
+ pub id: u32,
+ /// The type of the tile.
+ /// Refers to an object type and is used by tile objects. (optional) (since 1.0)
+ // TODO pub tile_type: Option<>,
+
+ /// Defines the terrain type of each corner of the tile,
+ /// given as comma-separated indexes in the terrain types array
+ /// in the order top-left, top-right, bottom-left, bottom-right.
+ /// Leaving out a value means that corner has no terrain. (optional)
+ // TODO pub terrain: Option<>,
+
+ /// A percentage indicating the probability that this tile is chosen
+ /// when it competes with others while editing with the terrain tool.
+ /// (defaults to 0)
+ pub probability: f64,
+
+ pub properties: super::property::Properties,
+
+ //TODO Can contain at most one: <image> (since 0.9), <objectgroup>, <animation>
+ pub image: Option<super::image::ImageSource>,
+
+ /// 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>>,
+}
+
+/// 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.
+#[derive(Eq, PartialEq, Debug)]
+pub struct TileOffset {
+ /// Horizontal offset in pixels. (defaults to 0)
+ x: isize,
+ /// Vertical offset in pixels (positive is down, defaults to 0)
+ y: isize,
+}
+
+impl TileOffset {
+ fn from_xml(node: roxmltree::Node) -> anyhow::Result<TileOffset> {
+ ensure_element!(node);
+ ensure_tag_name!(node, "tileoffset");
+
+ Ok(TileOffset {
+ x: get_attribute!(node, "x")?.parse()?,
+ y: get_attribute!(node, "y")?.parse()?,
+ })
+ }
+}
+
+impl Default for TileOffset {
+ fn default() -> Self {
+ TileOffset { x: 0, y: 0 }
+ }
+}
+
+#[cfg(test)]
+mod test_tile_offset {
+ use crate::lowlevel::tileset::TileOffset;
+
+ #[test]
+ fn default_value() {
+ let default: TileOffset = Default::default();
+ assert_eq!(default.x, 0);
+ assert_eq!(default.y, 0);
+ }
+
+ #[test]
+ fn from_xml() {
+ crate::parse_property_test!(
+ TileOffset,
+ r##"<tileoffset x="32" y="16" />"##,
+ TileOffset { x: 32, y: 16 }
+ );
+ }
+}
+
+/// Orientation of the grid for the tiles in this tileset (orthogonal or isometric, defaults to orthogonal)
+#[derive(Eq, PartialEq, Debug)]
+pub enum GridOrientation {
+ Orthogonal,
+ Isometric,
+}
+
+impl Default for GridOrientation {
+ fn default() -> Self {
+ GridOrientation::Orthogonal
+ }
+}
+
+/// This element is only used in case of isometric orientation,
+/// and determines how tile overlays for terrain and collision information are rendered.
+#[derive(Eq, PartialEq, Debug)]
+pub struct Grid {
+ orientation: GridOrientation,
+ /// Width of a grid cell
+ width: u32,
+ /// Height of a grid cell
+ height: u32,
+}
+
+impl Grid {
+ fn from_xml(node: roxmltree::Node) -> anyhow::Result<Grid> {
+ ensure_element!(node);
+ ensure_tag_name!(node, "grid");
+ let orientation: GridOrientation = match node.attribute("orientation") {
+ Some("orthogonal") => GridOrientation::Orthogonal,
+ Some("isometric") => GridOrientation::Isometric,
+ _ => GridOrientation::default(),
+ };
+
+ Ok(Grid {
+ orientation,
+ width: get_attribute!(node, "width")?.parse()?,
+ height: get_attribute!(node, "height")?.parse()?,
+ })
+ }
+}
+
+#[cfg(test)]
+mod test_grid {
+ use crate::lowlevel::tileset::{Grid, GridOrientation};
+
+ #[test]
+ fn grid_orientation_default_value() {
+ let default: GridOrientation = Default::default();
+ assert_eq!(default, GridOrientation::Orthogonal);
+ }
+
+ #[test]
+ fn from_xml() {
+ crate::parse_property_test!(
+ Grid,
+ r##"<grid orientation="isometric" height="32" width="32" />"##,
+ Grid {
+ orientation: GridOrientation::Isometric,
+ height: 32,
+ width: 32
+ }
+ );
+ crate::parse_property_test!(
+ Grid,
+ r##"<grid orientation="orthogonal" height="32" width="32" />"##,
+ Grid {
+ orientation: GridOrientation::Orthogonal,
+ height: 32,
+ width: 32
+ }
+ );
+ }
+
+ #[test]
+ fn from_xml_without_explicit_orientation() {
+ crate::parse_property_test!(
+ Grid,
+ r##"<grid height="32" width="32" />"##,
+ Grid {
+ orientation: GridOrientation::default(),
+ height: 32,
+ width: 32
+ }
+ );
+ }
+}
+
+#[derive(PartialEq, Debug)]
+pub struct Terrain {
+ /// The name of the terrain type.
+ pub name: String,
+ /// The local tile-id of the tile that represents the terrain visually.
+ pub tile: u32,
+ pub properties: Properties,
+}
+
+impl Terrain {
+ fn from_xml(node: roxmltree::Node) -> anyhow::Result<Terrain> {
+ ensure_element!(node);
+ ensure_tag_name!(node, "terrain");
+
+ let name = get_attribute!(node, "name")?.to_string();
+ let tile = get_attribute!(node, "tile")?.parse()?;
+ let properties: Properties = match node.children().find(|&c| c.has_tag_name("properties")) {
+ Some(node) => Properties::from_xml(node)?,
+ None => Properties::default(),
+ };
+
+ Ok(Terrain {
+ name,
+ tile,
+ 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,
+}
+
+/// “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).
+#[derive(PartialEq, Debug)]
+pub struct TileSetTransformations {
+ /// Whether the tiles in this set can be flipped horizontally (default 0)
+ pub hflip: bool,
+ /// Whether the tiles in this set can be flipped vertically (default 0)
+ pub vflip: bool,
+ /// Whether the tiles in this set can be rotated in 90 degree increments (default 0)
+ pub rotate: bool,
+ /// Whether untransformed tiles remain preferred, otherwise transformed tiles are used to produce more variations (default 0)
+ pub prefer_untransformed: bool,
+}
+
+impl Default for TileSetTransformations {
+ fn default() -> Self {
+ TileSetTransformations {
+ hflip: false,
+ vflip: false,
+ rotate: false,
+ prefer_untransformed: false,
+ }
+ }
+}
+
+impl TileSetTransformations {
+ pub fn from_xml(node: roxmltree::Node) -> anyhow::Result<TileSetTransformations> {
+ ensure_element!(node);
+ ensure_tag_name!(node, "transformations");
+ macro_rules! num_string_to_bool {
+ ($attribute_name:expr) => {
+ match node.attribute($attribute_name) {
+ Some("1") => Ok(true),
+ Some("0") => Ok(false),
+ Some(val) => Err(anyhow!(
+ "Unexpected value \"{}\" for {}. Only 0->false and 1->true are allowed",
+ val,
+ $attribute_name
+ )),
+ None => Ok(false),
+ }
+ };
+ }
+
+ Ok(TileSetTransformations {
+ hflip: num_string_to_bool!("hflip")?,
+ vflip: num_string_to_bool!("vflip")?,
+ rotate: num_string_to_bool!("rotate")?,
+ prefer_untransformed: num_string_to_bool!("preferuntransformed")?,
+ })
+ }
+}
+
+//TODO TileSetTransformations tests (parse test and test for default)
+
+#[cfg(test)]
+mod test_transformations {
+ use crate::lowlevel::tileset::TileSetTransformations;
+
+ #[test]
+ fn default_value() {
+ let default: TileSetTransformations = Default::default();
+ assert_eq!(
+ default,
+ TileSetTransformations {
+ hflip: false,
+ vflip: false,
+ rotate: false,
+ prefer_untransformed: false,
+ }
+ );
+ }
+
+ #[test]
+ fn from_xml() {
+ crate::parse_property_test!(
+ TileSetTransformations,
+ r##"<transformations hflip="1" vflip="1" rotate="1" preferuntransformed="1" />"##,
+ TileSetTransformations {
+ hflip: true,
+ vflip: true,
+ rotate: true,
+ prefer_untransformed: true,
+ }
+ );
+ crate::parse_property_test!(
+ TileSetTransformations,
+ r##"<transformations hflip="1" vflip="0" rotate="1" preferuntransformed="0" />"##,
+ TileSetTransformations {
+ hflip: true,
+ vflip: false,
+ rotate: true,
+ prefer_untransformed: false,
+ }
+ );
+ }
+}
+
+pub struct TileSet {
+ /// The name of this tileset.
+ pub name: String,
+ /// The (maximum) width of the tiles in this tileset.
+ pub tile_width: usize,
+
+ /// The (maximum) height of the tiles in this tileset.
+ pub tile_height: usize,
+ /// The spacing in pixels between the tiles in this tileset (applies to the tileset image, defaults to 0)
+ pub spacing: usize,
+
+ /// The margin around the tiles in this tileset (applies to the tileset image, defaults to 0)
+ pub margin: usize,
+
+ /// The number of tiles in this tileset (since 0.13)
+ pub tile_count: usize,
+
+ /// The number of tile columns in the tileset.
+ /// For image collection tilesets it is editable and is used when displaying the tileset. (since 0.15)
+ pub columns: usize,
+
+ /// Controls the alignment for tile objects.
+ /// Valid values are unspecified, topleft, top, topright, left, center, right, bottomleft, bottom and bottomright.
+ /// The default value is unspecified, for compatibility reasons.
+ /// When unspecified, tile objects use bottomleft in orthogonal mode and bottom in isometric mode. (since 1.4)
+ pub object_alignment: ObjectAlignment,
+
+ /// Define properties for specific tiles.
+ /// This doesn't necessarily contain all tiles (only tiles that have properties, like animations or image tiles)
+ /// In fact most of the time it doesn't contain tiles.
+ pub tile: Vec<Tile>,
+
+ pub image: Option<super::image::Image>,
+
+ /// this field is optional, if not set it will get {x:0, y:0} (no offset)
+ pub tile_offset: TileOffset,
+
+ /// (since 1.0)
+ pub grid: Option<Grid>,
+
+ pub properties: super::property::Properties,
+
+ pub terrain_types: Option<Vec<Terrain>>,
+
+ /// (since 1.1)
+ pub wangsets: Option<Vec<WangSet>>,
+
+ /// (since 1.5)
+ pub transformations: TileSetTransformations,
+}
+
+pub enum TileSetElement {
+ Embedded { first_gid: usize, tileset: TileSet },
+ External { first_gid: usize, source: String },
+}
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..ee455d9
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,34 @@
+use anyhow::anyhow;
+use rgb::RGBA;
+
+use std::io::prelude::*;
+use std::io::BufReader;
+use std::{fs::File, usize};
+
+mod lowlevel;
+
+fn main1() -> anyhow::Result<()> {
+ let file = File::open("./testing_data/tiled_examples/desert.tmx")?;
+ let mut reader = BufReader::new(file);
+
+ let mut string = "".to_owned();
+ reader.read_to_string(&mut string)?;
+
+ println!("{}", string);
+
+ let doc = roxmltree::Document::parse(&string)?;
+
+ for t in doc.root().children().enumerate() {
+ println!("{:?}", t);
+ }
+
+ let map = lowlevel::map::Map::from_xml(doc);
+
+ Ok(())
+}
+
+fn main() {
+ println!("{:?}", main1());
+}
+
+// https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#tileset