From a595ed8d9fbe6ff4a7a6f13c279b5412aeb41ab4 Mon Sep 17 00:00:00 2001 From: LawnCable Date: Fri, 15 Apr 2022 08:37:05 +0200 Subject: layer tiles, draft for object parsing and more --- src/lowlevel/data.rs | 1 - src/lowlevel/layer.rs | 44 ++++++++- src/lowlevel/layer_tile.rs | 73 +++++++++++++++ src/lowlevel/map.rs | 70 ++++++++++++-- src/lowlevel/mod.rs | 42 ++++++++- src/lowlevel/objects.draft.rs | 92 +++++++++++++++++++ src/lowlevel/tileset.rs | 208 +++++++++++++++++++++++++++++++++++++++--- src/main.rs | 8 +- 8 files changed, 506 insertions(+), 32 deletions(-) create mode 100644 src/lowlevel/layer_tile.rs create mode 100644 src/lowlevel/objects.draft.rs (limited to 'src') diff --git a/src/lowlevel/data.rs b/src/lowlevel/data.rs index e2833b5..59a0b3e 100644 --- a/src/lowlevel/data.rs +++ b/src/lowlevel/data.rs @@ -1,4 +1,3 @@ -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 { diff --git a/src/lowlevel/layer.rs b/src/lowlevel/layer.rs index ebb6cae..90fbb14 100644 --- a/src/lowlevel/layer.rs +++ b/src/lowlevel/layer.rs @@ -1,8 +1,10 @@ -pub struct LayerTile { - /// Global Tile Id - pub gid: u32, -} +use std::u32; + +use rgb::RGBA; + +use super::layer_tile::LayerTile; +#[derive(Debug)] pub struct LayerChunk { pub x: u32, pub y: u32, @@ -11,7 +13,41 @@ pub struct LayerChunk { pub data: Vec, } +#[derive(Debug)] pub enum LayerData { Tiles(Vec), Chunks(Vec), } + +#[derive(Debug)] +pub struct Layer { + /// Unique ID of the layer. Each layer that added to a map gets a unique id. Even if a layer is + /// deleted, no layer ever gets the same ID. Can not be changed in Tiled. (since Tiled 1.2) + pub id: u32, + /// The name of the layer. (defaults to “”) + pub name: String, + /// The x coordinate of the layer in tiles. Defaults to 0 and can not be changed in Tiled. + pub x: u32, + /// The y coordinate of the layer in tiles. Defaults to 0 and can not be changed in Tiled. + pub y: u32, + /// The width of the layer in tiles. Always the same as the map width for fixed-size maps. + pub width: u32, + /// The height of the layer in tiles. Always the same as the map height for fixed-size maps. + pub height: u32, + /// The opacity of the layer as a value from 0 to 1. Defaults to 1. + pub opacity: f32, + /// Whether the layer is shown (1) or hidden (0). Defaults to 1. + pub visible: bool, + /// A tint color that is multiplied with any tiles drawn by this layer in #AARRGGBB or #RRGGBB format (optonal). + pub tintcolor: Option>, + /// Horizontal offset for this layer in pixels. Defaults to 0. (since 0.14) + pub offsetx: u32, + /// Vertical offset for this layer in pixels. Defaults to 0. (since 0.14) + pub offsety: u32, + // /// Horizontal parallax factor for this layer. Defaults to 1. (since 1.5) + // pub parallaxx: ? + // /// Vertical parallax factor for this layer. Defaults to 1. (since 1.5) + // pub parallaxy: ? + pub properties: Option, + pub data: LayerData, +} diff --git a/src/lowlevel/layer_tile.rs b/src/lowlevel/layer_tile.rs new file mode 100644 index 0000000..bca5daf --- /dev/null +++ b/src/lowlevel/layer_tile.rs @@ -0,0 +1,73 @@ +// Bits on the far end of the 32-bit global tile ID are used for tile flags +const FLIPPED_HORIZONTALLY_FLAG: u32 = 0x80000000; +const FLIPPED_VERTICALLY_FLAG: u32 = 0x40000000; +const FLIPPED_DIAGONALLY_FLAG: u32 = 0x20000000; + +#[derive(Debug, Default, PartialEq)] +pub struct TileFlip { + // whether tile is flipped diagonally (should be the first operation) + pub diagonally: bool, + // whether tile is flipped horizontally (should be the second operation) + pub horizontally: bool, + // whether tile is flipped vertically (should be the third operation) + pub vertically: bool, +} + +impl TileFlip { + fn new(diagonally: bool, horizontally: bool, vertically: bool) -> Self { + Self { + diagonally, + horizontally, + vertically, + } + } +} + +#[derive(Debug)] +pub struct LayerTile(u32); + +impl LayerTile { + /// Global Tile Id + pub fn gid(&self) -> u32 { + // Clear the flags + self.0 & !(FLIPPED_HORIZONTALLY_FLAG | FLIPPED_VERTICALLY_FLAG | FLIPPED_DIAGONALLY_FLAG) + } + + pub fn get_flip(&self) -> TileFlip { + // Read out the flags + TileFlip { + diagonally: (self.0 & FLIPPED_DIAGONALLY_FLAG) > 0, + horizontally: (self.0 & FLIPPED_HORIZONTALLY_FLAG) > 0, + vertically: (self.0 & FLIPPED_VERTICALLY_FLAG) > 0, + } + } +} + +#[cfg(test)] +mod test_layer_tile { + use crate::lowlevel::{layer_tile::*}; + + #[test] + fn test_get_flip() { + assert_eq!(LayerTile(13).get_flip(), TileFlip::default()); + assert_eq!( + LayerTile(3451 | FLIPPED_DIAGONALLY_FLAG).get_flip(), + TileFlip::new(true, false, false) + ); + } + + #[test] + fn test_gid() { + assert_eq!(LayerTile(5).gid(), 5); + assert_eq!( + LayerTile( + 52 | (FLIPPED_HORIZONTALLY_FLAG + | FLIPPED_VERTICALLY_FLAG + | FLIPPED_DIAGONALLY_FLAG) + ) + .gid(), + 52 + ); + assert_eq!(LayerTile(798 | (FLIPPED_DIAGONALLY_FLAG)).gid(), 798); + } +} diff --git a/src/lowlevel/map.rs b/src/lowlevel/map.rs index 63cc9e1..9d0b715 100644 --- a/src/lowlevel/map.rs +++ b/src/lowlevel/map.rs @@ -1,10 +1,15 @@ +use crate::{ensure_element, ensure_tag_name, get_attribute}; use anyhow::anyhow; use rgb::RGBA; +use super::property::Properties; +use super::tileset::TileSetElement; + // https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#map /// Map orientation /// Tiled supports “orthogonal”, “isometric”, “staggered” and “hexagonal” (since 0.11). +#[derive(Debug)] pub enum MapOrientation { Orthogonal, Isometric, @@ -85,6 +90,7 @@ impl MapOrientation { /// The order in which tiles on tile layers are rendered. /// In all cases, the map is drawn row-by-row. (only supported for orthogonal maps at the moment) +#[derive(Debug)] pub enum MapRenderOrder { /// RightDown - The default RightDown, @@ -121,6 +127,7 @@ impl MapRenderOrder { } /// For staggered and hexagonal maps, determines which axis (“x” or “y”) is staggered. (since 0.11) +#[derive(Debug)] pub enum StaggerAxis { X, Y, @@ -144,6 +151,7 @@ impl StaggerAxis { } /// For staggered and hexagonal maps, determines whether the “even” or “odd” indexes along the staggered axis are shifted. (since 0.11) +#[derive(Debug)] pub enum StaggerIndex { Even, Odd, @@ -166,13 +174,11 @@ impl StaggerIndex { } } +#[derive(Debug)] pub struct Map { /// The TMX format version. Was “1.0” so far, and will be incremented to match minor Tiled releases. 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 tiled_version: Option, - /// Map orientation. Tiled supports “orthogonal”, “isometric”, “staggered” and “hexagonal” (since 0.11). pub orientation: MapOrientation, @@ -180,7 +186,7 @@ pub struct Map { pub render_order: MapRenderOrder, /// The compression level to use for tile layer data (defaults to -1, which means to use the algorithm default). - pub compression_level: Option, // TODO seems optional, please validate + pub compression_level: isize, // The map width in tiles. pub width: usize, @@ -214,10 +220,60 @@ pub struct Map { impl Map { pub fn from_xml(doc: roxmltree::Document) -> anyhow::Result { - doc.root_element(); + let node = doc.root_element(); + + ensure_element!(node); + ensure_tag_name!(node, "map"); + + let version = get_attribute!(node, "version")?.to_owned(); + + let orientation = MapOrientation::from_xml_map_node(&node)?; + + let render_order = MapRenderOrder::from_string(get_attribute!(node, "renderorder")?)?; - assert!(doc.root_element().has_tag_name("map")); + let compression_level = if let Ok(clevel) = get_attribute!(node, "compressionlevel") { + clevel.parse()? + } else { + -1 + }; + + let infinite = if let Ok(inf) = get_attribute!(node, "infinite") { + match inf { + "1" => true, + "0" | _ => false, + } + } else { + false + }; + + 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 mut tilesets = Vec::new(); + + for tileset_node in node.children().filter(|n| n.has_tag_name("tileset")) { + println!("->{:?}", &tileset_node); + tilesets.push(TileSetElement::from_xml(tileset_node)?); + } - Err(anyhow!("not implemented")) + Ok(Map { + version, + orientation, + render_order, + compression_level, + width: get_attribute!(node, "width")?.parse()?, + height: get_attribute!(node, "height")?.parse()?, + tile_width: get_attribute!(node, "tilewidth")?.parse()?, + tile_height: get_attribute!(node, "tileheight")?.parse()?, + background_color: None, // TODO + next_layer_id: 4242, // TODO + next_object_id: 4242, // TODO + infinite, + properties, + tilesets, + layer: vec![], // TODO + }) } } diff --git a/src/lowlevel/mod.rs b/src/lowlevel/mod.rs index 824c693..2eb951e 100644 --- a/src/lowlevel/mod.rs +++ b/src/lowlevel/mod.rs @@ -1,3 +1,9 @@ +use anyhow::{anyhow, bail}; + +use crate::get_attribute; + +use self::tileset::TileSetElement; + pub mod map; pub mod property; @@ -9,7 +15,41 @@ pub mod image; pub mod data; pub mod layer; +pub mod layer_tile; pub(crate) mod macros; -pub mod wangset; \ No newline at end of file +pub mod wangset; + +pub fn parse_map(input: &str) -> anyhow::Result { + let doc = roxmltree::Document::parse(input).unwrap(); + map::Map::from_xml(doc) +} + +pub fn parse_tileset(input: &str) -> anyhow::Result { + let doc = roxmltree::Document::parse(input).unwrap(); + let elem = doc.root_element(); + tileset::TileSet::from_xml(&elem) +} + +pub fn get_tile_set_elements_from_map(input: &str) -> anyhow::Result> { + let doc = roxmltree::Document::parse(input).unwrap(); + let node = doc.root_element(); + let mut tilesets = Vec::new(); + for tileset_node in node.children().filter(|n| n.has_tag_name("tileset")) { + tilesets.push(TileSetElement::from_xml(tileset_node)?); + } + Ok(tilesets) +} + +/// The Tiled version used to save the file (since Tiled 1.0.1). May be a date (for snapshot builds). (optional) +fn parse_tiled_version(node: &roxmltree::Node) -> anyhow::Result> { + if !node.is_root() { + bail!("not a root node") + } + + Ok(match get_attribute!(node, "tiledversion") { + Ok(v) => Some(v.to_owned()), + Err(_) => None, + }) +} diff --git a/src/lowlevel/objects.draft.rs b/src/lowlevel/objects.draft.rs new file mode 100644 index 0000000..9416e27 --- /dev/null +++ b/src/lowlevel/objects.draft.rs @@ -0,0 +1,92 @@ +enum TextHAlign { + Left, + Center, + Right, + Justify, +} + +impl Default for TextHAlign { + fn default() -> Self { + TextHAlign::Left + } +} + +enum TextVAlign { + Top, + Center, + Bottom, +} + +impl Default for TextVAlign { + fn default() -> Self { + TextVAlign::Top + } +} + +enum Object { + Tile { + gid: usize, + }, + Ellipse, + Point, + Polygon { + points: Vec<(i64, i64)>, + }, + Polyline { + points: Vec<(i64, i64)>, + }, + Text { + /// The font family used (defaults to “sans-serif”) + fonfamily: String, + /// The size of the font in pixels (not using points, because other sizes in the TMX format are also using pixels) (defaults to 16) + pixelsize: u32, + + /// Whether word wrapping is enabled (1) or disabled (0). (defaults to 0) + wrap: bool, + + /// Color of the text in #AARRGGBB or #RRGGBB format (defaults to #000000) + color: rgb::RGBA8, + + /// Whether the font is bold (1) or not (0). (defaults to 0) + bold: bool, + /// Whether the font is italic (1) or not (0). (defaults to 0) + italic: bool, + /// Whether a line should be drawn below the text (1) or not (0). (defaults to 0) + underline: bool, + /// Whether a line should be drawn through the text (1) or not (0). (defaults to 0) + strikeout: bool, + /// Whether kerning should be used while rendering the text (1) or not (0). (defaults to 1) + kerning: bool, + + /// Horizontal alignment of the text within the object ( left, center, right or justify, defaults to left) (since Tiled 1.2.1) + halign: TextHAlign, + /// Vertical alignment of the text within the object ( top, center or bottom, defaults to top) + valign: TextVAlign, + }, +} + +struct BaseObject { + object: Object, + /// Unique ID of the object. Each object that is placed on a map gets a unique id. Even if an object was deleted, no object gets the same ID. Can not be changed in Tiled. (since Tiled 0.11) + id: usize, + /// The name of the object. An arbitrary string. (defaults to “”) + name: String, + /// The type of the object. An arbitrary string. (defaults to “”) + r#type: String, + /// The x coordinate of the object in pixels. (defaults to 0) + x: u64, + /// The y coordinate of the object in pixels. (defaults to 0) + y: u64, + /// The width of the object in pixels. (defaults to 0) + width: u64, + /// The height of the object in pixels. (defaults to 0) + height: u64, + /// The rota�on of the object in degrees clockwise around (x, y). (defaults to 0) + rotation: ?, + /// Whether the object is shown (1) or hidden (0). (defaults to 1) + visible: bool, + /// A reference to a template file. (op�onal) + template: Option +} + +// todo ask in tiled discord if representation is ok diff --git a/src/lowlevel/tileset.rs b/src/lowlevel/tileset.rs index 9db86c8..ab96af4 100644 --- a/src/lowlevel/tileset.rs +++ b/src/lowlevel/tileset.rs @@ -1,5 +1,5 @@ use crate::{ensure_element, ensure_tag_name, get_attribute}; -use anyhow::anyhow; +use anyhow::{anyhow, bail, Context}; use super::image::Image; use super::property::Properties; @@ -49,10 +49,10 @@ impl ObjectAlignment { #[derive(Debug, PartialEq)] pub struct Frame { /// The local ID of a tile within the parent . - tileid: u32, + pub tileid: u32, // How long (in milliseconds) this frame should be displayed before advancing to the next frame. - duration: u32, + pub duration: u32, } impl Frame { @@ -413,7 +413,7 @@ 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 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, } @@ -437,7 +437,6 @@ impl Terrain { } } - /// 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)] @@ -538,6 +537,9 @@ mod test_transformations { #[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. @@ -579,10 +581,10 @@ pub struct TileSet { pub properties: super::property::Properties, - pub terrain_types: Option>, + pub terrain_types: Vec, /// (since 1.1) - pub wangsets: Option>, + pub wangsets: Vec, /// (since 1.5) pub transformations: TileSetTransformations, @@ -637,22 +639,116 @@ impl TileSet { 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: get_attribute!(node, "tilewidth")?.parse()?, - tile_height: get_attribute!(node, "tileheight")?.parse()?, + tile_width, + tile_height, spacing, margin, - tile_count: get_attribute!(node, "tilecount")?.parse()?, - columns: get_attribute!(node, "columns")?.parse()?, + tile_count, + columns, object_alignment, tile: tiles, image, tile_offset, grid, properties, - terrain_types: None, // TODO - wangsets: None, // TODO + terrain_types, + wangsets, transformations, }) } @@ -673,6 +769,7 @@ mod test_tileset { assert_eq!( tile_set, TileSet { + version: None, name: "beach_tileset".to_owned(), tile_width: 16, tile_height: 16, @@ -742,8 +839,8 @@ mod test_tileset { tile_offset: TileOffset::default(), grid: None, properties: Properties::default(), - terrain_types: None, - wangsets: None, + terrain_types: vec![], + wangsets: vec![], transformations: TileSetTransformations::default() } ); @@ -752,7 +849,88 @@ mod test_tileset { // todo more tests } +/// represents a tileset element inside of 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() + } + ); + } +} diff --git a/src/main.rs b/src/main.rs index ee455d9..1fab2ec 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,8 +7,8 @@ use std::{fs::File, usize}; mod lowlevel; -fn main1() -> anyhow::Result<()> { - let file = File::open("./testing_data/tiled_examples/desert.tmx")?; +fn main1() -> anyhow::Result { + let file = File::open("./testing_data/018-4.tmx")?; let mut reader = BufReader::new(file); let mut string = "".to_owned(); @@ -22,9 +22,9 @@ fn main1() -> anyhow::Result<()> { println!("{:?}", t); } - let map = lowlevel::map::Map::from_xml(doc); + let map = lowlevel::map::Map::from_xml(doc)?; - Ok(()) + Ok(map) } fn main() { -- cgit v1.2.3-60-g2f50