Skip to content
Snippets Groups Projects
map.rs 10.7 KiB
Newer Older
//! Structures related to Tiled maps.

use std::{collections::HashMap, fmt, fs::File, io::Read, path::Path, str::FromStr, sync::Arc};
alexdevteam's avatar
alexdevteam committed

Alejandro Perea's avatar
Alejandro Perea committed
use xml::attribute::OwnedAttribute;
alexdevteam's avatar
alexdevteam committed

use crate::{
    layers::{LayerData, LayerTag},
alexdevteam's avatar
alexdevteam committed
    properties::{parse_properties, Color, Properties},
alexdevteam's avatar
alexdevteam committed
    tileset::Tileset,
    util::{get_attrs, parse_tag, XmlEventResult},
    EmbeddedParseResultType, Layer, ResourceCache,
alexdevteam's avatar
alexdevteam committed
};

pub(crate) struct MapTilesetGid {
    pub first_gid: Gid,
    pub tileset: Arc<Tileset>,
/// All Tiled map files will be parsed into this. Holds all the layers and tilesets.
#[derive(PartialEq, Clone, Debug)]
alexdevteam's avatar
alexdevteam committed
pub struct Map {
    version: String,
    /// The way tiles are laid out in the map.
alexdevteam's avatar
alexdevteam committed
    pub orientation: Orientation,
alexdevteam's avatar
alexdevteam committed
    /// Width of the map, in tiles.
    ///
    /// ## Note
    /// There is no guarantee that this value will be the same as the width from its tile layers.
alexdevteam's avatar
alexdevteam committed
    pub width: u32,
alexdevteam's avatar
alexdevteam committed
    /// Height of the map, in tiles.
    ///
    /// ## Note
    /// There is no guarantee that this value will be the same as the height from its tile layers.
alexdevteam's avatar
alexdevteam committed
    pub height: u32,
alexdevteam's avatar
alexdevteam committed
    /// Tile width, in pixels.
    ///
    /// ## Note
    /// This value along with [`Self::tile_height`] determine the general size of the map, and
    /// individual tiles may have different sizes. As such, there is no guarantee that this value
    /// will be the same as the one from the tilesets the map is using.
alexdevteam's avatar
alexdevteam committed
    pub tile_width: u32,
alexdevteam's avatar
alexdevteam committed
    /// Tile height, in pixels.
    ///
    /// ## Note
    /// This value along with [`Self::tile_width`] determine the general size of the map, and
    /// individual tiles may have different sizes. As such, there is no guarantee that this value
    /// will be the same as the one from the tilesets the map is using.
alexdevteam's avatar
alexdevteam committed
    pub tile_height: u32,
    /// The tilesets present on this map.
    tilesets: Vec<Arc<Tileset>>,
    /// The layers present in this map.
    layers: Vec<LayerData>,
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>,
    infinite: bool,
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
alexdevteam's avatar
alexdevteam committed
    /// parse it. This augments `parse_file` with a custom reader: some engines
alexdevteam's avatar
alexdevteam committed
    /// (e.g. Amethyst) simply hand over a byte stream (and file location) for parsing,
    /// in which case this function may be required.
alexdevteam's avatar
alexdevteam committed
    ///
    /// The path is used for external dependencies such as tilesets or images. It is required.
    /// If the map if fully embedded and doesn't refer to external files, you may input an arbitrary path;
    /// the library won't read from the filesystem if it is not required to do so.
    ///
    /// The tileset cache is used to store and refer to any tilesets found along the way.
    #[deprecated(since = "0.10.1", note = "Use `Loader::load_tmx_map_from` instead")]
    pub fn parse_reader<R: Read>(
        reader: R,
        path: impl AsRef<Path>,
        cache: &mut impl ResourceCache,
Alejandro Perea's avatar
Alejandro Perea committed
        crate::parse::xml::parse_map(reader, path.as_ref(), cache)
alexdevteam's avatar
alexdevteam committed
    }

alexdevteam's avatar
alexdevteam committed
    /// Parse a file hopefully containing a Tiled map and try to parse it.  All external
    /// files will be loaded relative to the path given.
    ///
    /// The tileset cache is used to store and refer to any tilesets found along the way.
    #[deprecated(since = "0.10.1", note = "Use `Loader::load_tmx_map` instead")]
    pub fn parse_file(path: impl AsRef<Path>, cache: &mut impl ResourceCache) -> Result<Self> {
        let reader = File::open(path.as_ref()).map_err(|err| Error::CouldNotOpenFile {
Alejandro Perea's avatar
Alejandro Perea committed
            path: path.as_ref().to_owned(),
            err,
        })?;
Alejandro Perea's avatar
Alejandro Perea committed
        crate::parse::xml::parse_map(reader, path.as_ref(), cache)

    /// The TMX format version this map was saved to. Equivalent to the map file's `version`
    /// attribute.
    pub fn version(&self) -> &str {
        self.version.as_ref()
    }

    /// Whether this map is infinite. An infinite map has no fixed size and can grow in all
    /// directions. Its layer data is stored in chunks. This value determines whether the map's
    /// tile layers are [`FiniteTileLayer`](crate::FiniteTileLayer)s or [`crate::InfiniteTileLayer`](crate::InfiniteTileLayer)s.
    pub fn infinite(&self) -> bool {
        self.infinite
    }
}

impl Map {
    /// Get a reference to the map's tilesets.
    pub fn tilesets(&self) -> &[Arc<Tileset>] {
        self.tilesets.as_ref()
    }

    /// Get an iterator over all the layers in the map in ascending order of their layer index.
    pub fn layers(&self) -> impl ExactSizeIterator<Item = Layer> {
        self.layers.iter().map(move |layer| Layer::new(self, layer))
alexdevteam's avatar
alexdevteam committed
    }

    /// Returns the layer that has the specified index, if it exists.
    pub fn get_layer(&self, index: usize) -> Option<Layer> {
        self.layers.get(index).map(|data| Layer::new(self, data))
    }
}

impl Map {
Alejandro Perea's avatar
Alejandro Perea committed
    pub(crate) fn parse_xml(
        parser: &mut impl Iterator<Item = XmlEventResult>,
alexdevteam's avatar
alexdevteam committed
        attrs: Vec<OwnedAttribute>,
        map_path: &Path,
        cache: &mut impl ResourceCache,
alexdevteam's avatar
alexdevteam committed
        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()),
            ],
            Error::MalformedAttributes("map must have version, width, height, tilewidth, tileheight and orientation with correct types".to_string())
alexdevteam's avatar
alexdevteam committed
        );

        let infinite = infinite.unwrap_or(false);
alexdevteam's avatar
alexdevteam committed

        // We can only parse sequentally, but tilesets are guaranteed to appear before layers.
        // So we can pass in tileset data to layer construction without worrying about unfinished
        // data usage.
alexdevteam's avatar
alexdevteam committed
        let mut layers = Vec::new();
        let mut properties = HashMap::new();
        let mut tilesets = Vec::new();

alexdevteam's avatar
alexdevteam committed
        parse_tag!(parser, "map", {
            "tileset" => |attrs| {
                let res = Tileset::parse_xml_in_map(parser, attrs, map_path)?;
                match res.result_type {
                    EmbeddedParseResultType::ExternalReference { tileset_path } => {
                        let file = File::open(&tileset_path).map_err(|err| Error::CouldNotOpenFile{path: tileset_path.clone(), err })?;
Alejandro Perea's avatar
Alejandro Perea committed
                        let tileset = cache.get_or_try_insert_tileset_with(tileset_path.clone(), || crate::parse::xml::parse_tileset(file, &tileset_path))?;
                        tilesets.push(MapTilesetGid{first_gid: res.first_gid, tileset});
                    }
                    EmbeddedParseResultType::Embedded { tileset } => {
                        tilesets.push(MapTilesetGid{first_gid: res.first_gid, tileset: Arc::new(tileset)});
alexdevteam's avatar
alexdevteam committed
                Ok(())
            },
            "layer" => |attrs| {
                layers.push(LayerData::new(
                    parser,
                    attrs,
                    LayerTag::TileLayer,
                    infinite,
                    map_path,
                    &tilesets,
                )?);
alexdevteam's avatar
alexdevteam committed
                Ok(())
            },
            "imagelayer" => |attrs| {
                layers.push(LayerData::new(
                    parser,
                    attrs,
                    LayerTag::ImageLayer,
                    infinite,
                    map_path,
                    &tilesets,
                )?);
alexdevteam's avatar
alexdevteam committed
                Ok(())
            },
            "objectgroup" => |attrs| {
                layers.push(LayerData::new(
                    parser,
                    attrs,
                    LayerTag::ObjectLayer,
                    infinite,
                    map_path,
                    &tilesets,
                )?);
                Ok(())
            },
            "group" => |attrs| {
                layers.push(LayerData::new(
                    parser,
                    attrs,
                    LayerTag::GroupLayer,
                    infinite,
                    map_path,
alexdevteam's avatar
alexdevteam committed
                Ok(())
            },
            "properties" => |_| {
                properties = parse_properties(parser)?;
alexdevteam's avatar
alexdevteam committed
                Ok(())
            },
        });

        // We do not need first GIDs any more
        let tilesets = tilesets.into_iter().map(|ts| ts.tileset).collect();

alexdevteam's avatar
alexdevteam committed
        Ok(Map {
            version: v,
            orientation: o,
            width: w,
            height: h,
            tile_width: tw,
            tile_height: th,
            tilesets,
            layers,
            properties,
alexdevteam's avatar
alexdevteam committed
            background_color: c,
/// Represents the way tiles are laid out in a map.
alexdevteam's avatar
alexdevteam committed
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
#[allow(missing_docs)]
alexdevteam's avatar
alexdevteam committed
pub enum Orientation {
    Orthogonal,
    Isometric,
    Staggered,
    Hexagonal,
}

impl FromStr for Orientation {
Alejandro Perea's avatar
Alejandro Perea committed
    type Err = ();
alexdevteam's avatar
alexdevteam committed

    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
alexdevteam's avatar
alexdevteam committed
        match s {
            "orthogonal" => Ok(Orientation::Orthogonal),
            "isometric" => Ok(Orientation::Isometric),
            "staggered" => Ok(Orientation::Staggered),
            "hexagonal" => Ok(Orientation::Hexagonal),
Alejandro Perea's avatar
Alejandro Perea committed
            _ => Err(()),
alexdevteam's avatar
alexdevteam committed
        }
    }
}

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

/// A Tiled global tile ID.
///
/// These are used to identify tiles in a map. Since the map may have more than one tileset, an
/// unique mapping is required to convert the tiles' local tileset ID to one which will work nicely
/// even if there is more than one tileset.
///
/// Tiled also treats GID 0 as empty space, which means that the first tileset in the map will have
/// a starting GID of 1.
///
/// See also: https://doc.mapeditor.org/en/latest/reference/global-tile-ids/
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) struct Gid(pub u32);

impl Gid {
    /// The GID representing an empty tile in the map.
    #[allow(dead_code)]
    pub const EMPTY: Gid = Gid(0);
}