use crate::{ensure_element, ensure_tag_name, get_attribute}; use anyhow::{anyhow, bail, Context}; use super::image::Image; use super::property::Properties; use super::wangset::WangSet; #[derive(Debug, PartialEq)] pub enum ObjectAlignment { Unspecified, TopLeft, Top, TopRight, Left, Center, Right, BottomLeft, Bottom, BottomRight, } impl Default for ObjectAlignment { fn default() -> Self { ObjectAlignment::Unspecified } } 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 . pub tileid: u32, // How long (in milliseconds) this frame should be displayed before advancing to the next frame. pub 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, /// 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, 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. #[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 { 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 } ); } } /// 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 { 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: GridOrientation::Isometric, height: 32, width: 32 } ); crate::parse_property_test!( Grid, r##""##, Grid { orientation: GridOrientation::Orthogonal, height: 32, width: 32 } ); } #[test] fn from_xml_without_explicit_orientation() { crate::parse_property_test!( Grid, r##""##, 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: i32, // TODO find out why `-1` is allowed here and maybe if there is a better way to represent it in rust? pub properties: Properties, } impl Terrain { fn from_xml(node: roxmltree::Node) -> anyhow::Result { 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, }) } } /// 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 { 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##""##, TileSetTransformations { hflip: true, vflip: true, rotate: true, prefer_untransformed: true, } ); crate::parse_property_test!( TileSetTransformations, r##""##, TileSetTransformations { hflip: true, vflip: false, rotate: true, prefer_untransformed: false, } ); } } #[derive(Debug, PartialEq)] pub struct TileSet { /// The TSX format version. pub version: Option, /// 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, pub image: Option, /// 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, pub properties: super::property::Properties, pub terrain_types: Vec, /// (since 1.1) pub wangsets: Vec, /// (since 1.5) 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(), }; let mut wangsets = Vec::new(); if let Some(wangsets_node) = node.children().find(|&c| c.has_tag_name("wangsets")) { for wangset_node in wangsets_node .children() .filter(|n| n.has_tag_name("wangset")) { wangsets.push(WangSet::from_xml(wangset_node)?); } } let mut terrain_types = Vec::new(); if let Some(terraintypes_node) = node.children().find(|&c| c.has_tag_name("terraintypes")) { for terrain_node in terraintypes_node .children() .filter(|n| n.has_tag_name("terrain")) { terrain_types.push(Terrain::from_xml(terrain_node).with_context(|| { format!("Failed to parse terraintype from:\n{:?}", terrain_node,) })?); } } let version = if let Ok(v) = get_attribute!(node, "version") { Some(v.to_owned()) } else { None }; let tile_width = get_attribute!(node, "tilewidth")?.parse()?; let tile_height = get_attribute!(node, "tileheight")?.parse()?; let tile_count: usize = match get_attribute!(node, "tilecount") { Ok(tc_string) => tc_string.parse()?, Err(err) => { if let Some(img) = &image { // there is no tilecount, but an image so we can guess/calculate the tilecount // TODO TEST for this if spacing != 0 { bail!("tilecount not set, and calculationg it with spacing enabled is not implemented yet."); } if let Some(image_width) = img.width { if let Some(image_heigth) = img.height { let real_tile_with = tile_width + (margin * 2); let real_tile_height = tile_height + (margin * 2); let columns = image_width / real_tile_with; let rows = image_heigth / real_tile_height; columns * rows } else { bail!("tilecount not set, calculating failed: image has no height"); } } else { bail!("tilecount not set, calculating failed: image has no width"); } } else { return Err(err); } } }; let columns: usize = match get_attribute!(node, "columns") { Ok(c) => c.parse()?, Err(err) => { if let Some(img) = &image { // there is no columns attribute, but an image so we can guess/calculate the columns // TODO TEST for this if spacing != 0 { bail!("columns not set, and calculationg it with spacing enabled is not implemented yet."); } if let Some(image_width) = img.width { if let Some(image_heigth) = img.height { let real_tile_with = tile_width + (margin * 2); let real_tile_height = tile_height + (margin * 2); let columns = image_width / real_tile_with; let rows = image_heigth / real_tile_height; columns * rows } else { bail!("columns not set, calculating failed: image has no height"); } } else { bail!("columns not set, calculating failed: image has no width"); } } else { return Err(err); } } }; Ok(TileSet { version, name: get_attribute!(node, "name")?.to_owned(), tile_width, tile_height, spacing, margin, tile_count, columns, object_alignment, tile: tiles, image, tile_offset, grid, properties, terrain_types, wangsets, 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 { version: None, 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: vec![], wangsets: vec![], transformations: TileSetTransformations::default() } ); } // todo more tests } /// represents a tileset element inside a tilemap #[derive(Debug, PartialEq)] pub enum TileSetElement { Embedded { first_gid: usize, tileset: TileSet }, External { first_gid: usize, source: String }, } impl TileSetElement { pub fn from_xml(node: roxmltree::Node) -> anyhow::Result { ensure_element!(node); ensure_tag_name!(node, "tileset"); let first_gid = get_attribute!(node, "firstgid")?.parse()?; if let Ok(source) = get_attribute!(node, "source") { Ok(TileSetElement::External { first_gid, source: source.to_owned(), }) } else { Ok(TileSetElement::Embedded { first_gid, tileset: TileSet::from_xml(&node)?, }) } } } #[cfg(test)] mod test_tileset_element { use crate::lowlevel::image::*; use crate::lowlevel::tileset::*; #[test] fn embedded_tilemap() { crate::parse_property_test!( TileSetElement, r##" "##, TileSetElement::Embedded { first_gid: 1, tileset: TileSet { version: None, name: "hex mini".to_owned(), tile_width: 18, tile_height: 18, tile_offset: TileOffset { x: 0, y: 1 }, terrain_types: vec![], wangsets: vec![], spacing: 0, margin: 0, tile_count: 20, columns: 5, object_alignment: ObjectAlignment::default(), tile: vec![], image: Some(Image { source: ImageSource::External { source: "hexmini.png".to_owned() }, transparent_color: None, width: Some(106), height: Some(72) }), grid: None, properties: Properties::default(), transformations: TileSetTransformations::default(), } } ); } #[test] fn external_tilemap() { crate::parse_property_test!( TileSetElement, r##""##, TileSetElement::External { first_gid: 1, source: "beach_tileset.tsx".to_owned() } ); } }