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 --- src/lowlevel/tileset.rs | 441 +++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 378 insertions(+), 63 deletions(-) (limited to 'src/lowlevel/tileset.rs') 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 }, -- cgit v1.2.3-60-g2f50