summaryrefslogblamecommitdiff
path: root/src/lowlevel/tileset.rs
blob: 9db86c86c04b01f76664170bd467005b879cdc4f (plain) (tree)
1
2
3
4
5
6
7
8
9

                                                            
 
                        

                                


                            


















                                    




















                                                                         







                                                                                                    







































































                                                                                    
                                         
                           



















                                                                                    
                                           
 
                          




                                                                                      
















































































































                                                                                                    






































































































































































                                                                                                           


































































































                                                                                                                                 
                           


















































                                                                                                                      



































































































































































                                                                                                    



                                                    
use crate::{ensure_element, ensure_tag_name, get_attribute};
use anyhow::anyhow;

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<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,

    // How long (in milliseconds) this frame should be displayed before advancing to the next 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,
    /// 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<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.
#[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,
        })
    }
}


/// 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,
            }
        );
    }
}

#[derive(Debug, PartialEq)]
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,
}

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 },
}