Skip to content
Snippets Groups Projects
map.rs 7.09 KiB
Newer Older
alexdevteam's avatar
alexdevteam committed
use std::{
    collections::HashMap,
    fmt,
    fs::File,
    io::Read,
    path::{Path, PathBuf},
    str::FromStr,
};
alexdevteam's avatar
alexdevteam committed

alexdevteam's avatar
alexdevteam committed
use xml::{attribute::OwnedAttribute, reader::XmlEvent, EventReader};
alexdevteam's avatar
alexdevteam committed

use crate::{
    error::{ParseTileError, TiledError},
    layers::{ImageLayer, Layer},
    objects::ObjectGroup,
alexdevteam's avatar
alexdevteam committed
    properties::{parse_properties, Color, Properties},
alexdevteam's avatar
alexdevteam committed
    tileset::Tileset,
alexdevteam's avatar
alexdevteam committed
    util::{get_attrs, parse_tag},
alexdevteam's avatar
alexdevteam committed
};

/// All Tiled map files will be parsed into this. Holds all the layers and tilesets.
alexdevteam's avatar
alexdevteam committed
#[derive(Debug, PartialEq, Clone)]
pub struct Map {
alexdevteam's avatar
alexdevteam committed
    /// The TMX format version this map was saved to.
alexdevteam's avatar
alexdevteam committed
    pub version: String,
    pub orientation: Orientation,
alexdevteam's avatar
alexdevteam committed
    /// Width of the map, in tiles.
alexdevteam's avatar
alexdevteam committed
    pub width: u32,
alexdevteam's avatar
alexdevteam committed
    /// Height of the map, in tiles.
alexdevteam's avatar
alexdevteam committed
    pub height: u32,
alexdevteam's avatar
alexdevteam committed
    /// Tile width, in pixels.
alexdevteam's avatar
alexdevteam committed
    pub tile_width: u32,
alexdevteam's avatar
alexdevteam committed
    /// Tile height, in pixels.
alexdevteam's avatar
alexdevteam committed
    pub tile_height: u32,
alexdevteam's avatar
alexdevteam committed
    /// The tilesets present in this map.
alexdevteam's avatar
alexdevteam committed
    pub tilesets: Vec<Tileset>,
alexdevteam's avatar
alexdevteam committed
    /// The tile layers present in this map.
alexdevteam's avatar
alexdevteam committed
    pub layers: Vec<Layer>,
    pub image_layers: Vec<ImageLayer>,
    pub object_groups: Vec<ObjectGroup>,
alexdevteam's avatar
alexdevteam committed
    /// The custom properties of this map.
alexdevteam's avatar
alexdevteam committed
    pub properties: Properties,
alexdevteam's avatar
alexdevteam committed
    /// The background color of this map, if any.
    pub background_color: Option<Color>,
alexdevteam's avatar
alexdevteam committed
    pub infinite: bool,
alexdevteam's avatar
alexdevteam committed
    /// Where this map was loaded from.
    /// If fully embedded (loaded with path = `None`), this will return `None`.
    pub source: Option<PathBuf>,
alexdevteam's avatar
alexdevteam committed
}

impl Map {
alexdevteam's avatar
alexdevteam committed
    /// Parse a buffer hopefully containing the contents of a Tiled file and try to
    /// parse it. This augments `parse` with a file location: some engines
    /// (e.g. Amethyst) simply hand over a byte stream (and file location) for parsing,
    /// in which case this function may be required.
    /// The path may be skipped if the map is fully embedded (Doesn't refer to external files).
    pub fn parse_reader<R: Read>(reader: R, path: Option<&Path>) -> Result<Self, TiledError> {
        let mut parser = EventReader::new(reader);
        loop {
            match parser.next().map_err(TiledError::XmlDecodingError)? {
                XmlEvent::StartElement {
                    name, attributes, ..
                } => {
                    if name.local_name == "map" {
                        return Self::parse_xml(&mut parser, attributes, path);
                    }
                }
                XmlEvent::EndDocument => {
                    return Err(TiledError::PrematureEnd(
                        "Document ended before map was parsed".to_string(),
                    ))
                }
                _ => {}
            }
        }
    }

    /// Parse a file hopefully containing a Tiled map and try to parse it.  If the
    /// file has an external tileset, the tileset file will be loaded using a path
    /// relative to the map file's path.
    pub fn parse_file(path: impl AsRef<Path>) -> Result<Self, TiledError> {
        let file = File::open(path.as_ref())
            .map_err(|_| TiledError::Other(format!("Map file not found: {:?}", path.as_ref())))?;
        Self::parse_reader(file, Some(path.as_ref()))
alexdevteam's avatar
alexdevteam committed
    }

    fn parse_xml<R: Read>(
alexdevteam's avatar
alexdevteam committed
        parser: &mut EventReader<R>,
        attrs: Vec<OwnedAttribute>,
        map_path: Option<&Path>,
    ) -> Result<Map, TiledError> {
        let ((c, infinite), (v, o, w, h, tw, th)) = get_attrs!(
            attrs,
            optionals: [
                ("backgroundcolor", colour, |v:String| v.parse().ok()),
                ("infinite", infinite, |v:String| Some(v == "1")),
            ],
            required: [
                ("version", version, |v| Some(v)),
                ("orientation", orientation, |v:String| v.parse().ok()),
                ("width", width, |v:String| v.parse().ok()),
                ("height", height, |v:String| v.parse().ok()),
                ("tilewidth", tile_width, |v:String| v.parse().ok()),
                ("tileheight", tile_height, |v:String| v.parse().ok()),
            ],
            TiledError::MalformedAttributes("map must have a version, width and height with correct types".to_string())
        );

        let mut tilesets = Vec::new();
        let mut layers = Vec::new();
        let mut image_layers = Vec::new();
        let mut properties = HashMap::new();
        let mut object_groups = Vec::new();
        let mut layer_index = 0;
        parse_tag!(parser, "map", {
            "tileset" => |attrs| {
alexdevteam's avatar
alexdevteam committed
                tilesets.push(Tileset::parse_xml(parser, attrs, map_path)?);
alexdevteam's avatar
alexdevteam committed
                Ok(())
            },
            "layer" => |attrs| {
                layers.push(Layer::new(parser, attrs, w, layer_index, infinite.unwrap_or(false))?);
                layer_index += 1;
                Ok(())
            },
            "imagelayer" => |attrs| {
                image_layers.push(ImageLayer::new(parser, attrs, layer_index)?);
                layer_index += 1;
                Ok(())
            },
            "properties" => |_| {
                properties = parse_properties(parser)?;
                Ok(())
            },
            "objectgroup" => |attrs| {
                object_groups.push(ObjectGroup::new(parser, attrs, Some(layer_index))?);
                layer_index += 1;
                Ok(())
            },
        });
        Ok(Map {
            version: v,
            orientation: o,
            width: w,
            height: h,
            tile_width: tw,
            tile_height: th,
            tilesets,
            layers,
            image_layers,
            object_groups,
            properties,
alexdevteam's avatar
alexdevteam committed
            background_color: c,
alexdevteam's avatar
alexdevteam committed
            infinite: infinite.unwrap_or(false),
alexdevteam's avatar
alexdevteam committed
            source: map_path.and_then(|p| Some(p.to_owned())),
alexdevteam's avatar
alexdevteam committed
        })
    }

    /// This function will return the correct Tileset given a GID.
alexdevteam's avatar
alexdevteam committed
    pub fn tileset_by_gid(&self, gid: u32) -> Option<&Tileset> {
alexdevteam's avatar
alexdevteam committed
        let mut maximum_gid: i32 = -1;
        let mut maximum_ts = None;
        for tileset in self.tilesets.iter() {
            if tileset.first_gid as i32 > maximum_gid && tileset.first_gid <= gid {
                maximum_gid = tileset.first_gid as i32;
                maximum_ts = Some(tileset);
            }
        }
        maximum_ts
    }
}

/// Represents the way tiles are laid out in a map.
alexdevteam's avatar
alexdevteam committed
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
pub enum Orientation {
    Orthogonal,
    Isometric,
    Staggered,
    Hexagonal,
}

impl FromStr for Orientation {
    type Err = ParseTileError;

    fn from_str(s: &str) -> Result<Orientation, ParseTileError> {
        match s {
            "orthogonal" => Ok(Orientation::Orthogonal),
            "isometric" => Ok(Orientation::Isometric),
            "staggered" => Ok(Orientation::Staggered),
            "hexagonal" => Ok(Orientation::Hexagonal),
            _ => Err(ParseTileError::OrientationError),
        }
    }
}

impl fmt::Display for Orientation {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Orientation::Orthogonal => write!(f, "orthogonal"),
            Orientation::Isometric => write!(f, "isometric"),
            Orientation::Staggered => write!(f, "staggered"),
            Orientation::Hexagonal => write!(f, "hexagonal"),
        }
    }
}