Skip to content
Snippets Groups Projects
  • Alejandro Perea's avatar
    Hide GIDs as internal details (#135) · df98e9f3
    Alejandro Perea authored
    
    * Partial commit
    
    * Partial commit
    
    * Partial commit
    
    * Some suggested changes and fixed tests (except infinite)
    
    * Replaced `tile` member in `LayerTileRef` with a function to get an
      `Option<&Tile>`.
    
    * Replaced `Map::get_tile_by_gid` with `Map::get_tileset_for_gid`, which
      just returns the `Option<&Tileset>`. It also does a reverse search,
      which fixes the lookup in case an external tileset has grown since the
      map was last saved.
    
    * Replaced `Tileset::get_tile_by_gid` with `Tileset::get_tile`, since
      subtracting of the `first_gid` already happens when creating the
      `LayerTileRef`. Also, we eventually should remove `first_gid` from
      `Tileset`, since it should be possible to share a single tileset
      instance betweeen several maps.
    
    * Pre-allocate the tiles hash map for the expected number of tiles and
      use `or_default` instead of `or_insert_with(Default::default)`.
    
    * [nonbuilding] Move ownership of tilesets
    - Moves the ownership of tilesets from Map to an object implementing `TilesetCache`
    
    * Clean up
    
    * More cleanup
    
    * Organize layers into modules
    
    * Further modularization
    
    * Add layer wrappers
    
    * Implement `Clone + PartialEq + Debug` for wrappers
    
    * Fix example
    
    * Fix all tests except for test_infinite_tileset
    
    * Move layer utils to its own module
    
    * Better `Map::layers` documentation
    
    * `TilesetCache` -> `cache::ResourceCache`
    
    * Add `ResourcePath`, rename and add errors
    
    * Interface changes
    - Move embedded tilesets from cache to map
    - Store `Option<LayerTileData>` instead of `LayerTileData`
    
    * parser ->`&mut impl Iterator<Item=XmlEventResult>`
    
    * Document that tilesets are ordered by first GID
    
    * Fix the layer tiles using GIDs issue
    
    * Run `cargo fix`
    
    * Fix `test_infinite_tileset` tests
    
    * Implement a way to access object tile data
    
    * Rename `TiledWrapper` to `MapWrapper`
    
    * More efficient `get_or_try_insert_tileset_with`
    
    * Add `ResourcePathBuf`, use `Rc<Tileset>` in `Map`
    
    * Remove `MapTileset::first_gid`
    
    * Run `cargo fix`
    
    * Remove unrelated `Tileset` changes
    
    * Requested changes
    
    * Avoid reference counting when accessing tiles
    
    * Store tile data instead of (u32, u32)` in objects
    
    * Remove unneeded functions from `ResourceCache`
    
    * Address PR comments
    
    * Misc improvements in `layers::tile`
    
    * Improve example
    
    * Convert `LayerTile` properties to fields
    
    Co-authored-by: default avatarThorbjørn Lindeijer <bjorn@lindeijer.nl>
    Unverified
    df98e9f3
tileset.rs 10.31 KiB
use std::collections::HashMap;
use std::io::Read;
use std::path::{Path, PathBuf};

use xml::attribute::OwnedAttribute;
use xml::reader::XmlEvent;
use xml::EventReader;

use crate::error::TiledError;
use crate::image::Image;
use crate::properties::{parse_properties, Properties};
use crate::tile::Tile;
use crate::{util::*, Gid};

/// A tileset, usually the tilesheet image.
#[derive(Debug, PartialEq, Clone)]
pub struct Tileset {
    pub name: String,
    pub tile_width: u32,
    pub tile_height: u32,
    pub spacing: u32,
    pub margin: u32,
    pub tilecount: u32,
    pub columns: u32,

    /// A tileset can either:
    /// * have a single spritesheet `image` in `tileset` ("regular" tileset);
    /// * have zero images in `tileset` and one `image` per `tile` ("image collection" tileset).
    ///
    /// --------
    /// - Source: [tiled issue #2117](https://github.com/mapeditor/tiled/issues/2117)
    /// - Source: [`columns` documentation](https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#tileset)
    pub image: Option<Image>,

    /// All the tiles present in this tileset, indexed by their local IDs.
    pub tiles: HashMap<u32, Tile>,

    /// The custom properties of the tileset.
    pub properties: Properties,

    /// Where this tileset was loaded from.
    /// If fully embedded, this will return `None`.
    pub source: Option<PathBuf>,
}

pub(crate) enum EmbeddedParseResultType {
    ExternalReference { tileset_path: PathBuf },
    Embedded { tileset: Tileset },
}

pub(crate) struct EmbeddedParseResult {
    pub first_gid: Gid,
    pub result_type: EmbeddedParseResultType,
}

/// Internal structure for holding mid-parse information.
struct TilesetProperties {
    spacing: Option<u32>,
    margin: Option<u32>,
    tilecount: u32,
    columns: Option<u32>,
    name: String,
    tile_width: u32,
    tile_height: u32,
    /// The path all non-absolute paths are relative to.
    path_relative_to: Option<PathBuf>,
    source: Option<PathBuf>,
}

impl Tileset {
    /// Parse a buffer hopefully containing the contents of a Tiled tileset.
    pub fn parse<R: Read>(reader: R) -> Result<Self, TiledError> {
        Tileset::new_external(reader, None)
    }

    /// Parse a buffer hopefully containing the contents of a Tiled tileset.
    pub fn parse_with_path<R: Read>(reader: R, path: impl AsRef<Path>) -> Result<Self, TiledError> {
        Tileset::new_external(reader, Some(path.as_ref()))
    }

    pub fn get_tile(&self, id: u32) -> Option<&Tile> {
        self.tiles.get(&id)
    }
}

impl Tileset {
    pub(crate) fn new_external<R: Read>(file: R, path: Option<&Path>) -> Result<Self, TiledError> {
        let mut tileset_parser = EventReader::new(file);
        loop {
            match tileset_parser
                .next()
                .map_err(TiledError::XmlDecodingError)?
            {
                XmlEvent::StartElement {
                    name, attributes, ..
                } => {
                    if name.local_name == "tileset" {
                        return Self::parse_external_tileset(
                            &mut tileset_parser.into_iter(),
                            &attributes,
                            path,
                        );
                    }
                }
                XmlEvent::EndDocument => {
                    return Err(TiledError::PrematureEnd(
                        "Tileset Document ended before map was parsed".to_string(),
                    ))
                }
                _ => {}
            }
        }
    }

    pub(crate) fn parse_xml_in_map(
        parser: &mut impl Iterator<Item = XmlEventResult>,
        attrs: Vec<OwnedAttribute>,
        map_path: &Path,
    ) -> Result<EmbeddedParseResult, TiledError> {
        let path_relative_to = map_path.parent();
        Tileset::parse_xml_embedded(parser, &attrs, path_relative_to).or_else(|err| {
            if matches!(err, TiledError::MalformedAttributes(_)) {
                Tileset::parse_xml_reference(&attrs, path_relative_to)
            } else {
                Err(err)
            }
        })
    }

    /// Returns both the tileset and its first gid in the corresponding map.
    fn parse_xml_embedded(
        parser: &mut impl Iterator<Item = XmlEventResult>,
        attrs: &Vec<OwnedAttribute>,
        path_relative_to: Option<&Path>,
    ) -> Result<EmbeddedParseResult, TiledError> {
        let ((spacing, margin, columns), (tilecount, first_gid, name, tile_width, tile_height)) = get_attrs!(
           attrs,
           optionals: [
                ("spacing", spacing, |v:String| v.parse().ok()),
                ("margin", margin, |v:String| v.parse().ok()),
                ("columns", columns, |v:String| v.parse().ok()),
            ],
           required: [
                ("tilecount", tilecount, |v:String| v.parse().ok()),
                ("firstgid", first_gid, |v:String| v.parse().ok().map(|n| Gid(n))),
                ("name", name, |v| Some(v)),
                ("tilewidth", width, |v:String| v.parse().ok()),
                ("tileheight", height, |v:String| v.parse().ok()),
            ],
            TiledError::MalformedAttributes("tileset must have a firstgid, name tile width and height with correct types".to_string())
        );

        Self::finish_parsing_xml(
            parser,
            TilesetProperties {
                spacing,
                margin,
                name,
                path_relative_to: path_relative_to.map(Path::to_owned),
                columns,
                tilecount,
                tile_height,
                tile_width,
                source: None,
            },
        )
        .map(|tileset| EmbeddedParseResult {
            first_gid,
            result_type: EmbeddedParseResultType::Embedded { tileset },
        })
    }

    fn parse_xml_reference(
        attrs: &Vec<OwnedAttribute>,
        path_relative_to: Option<&Path>,
    ) -> Result<EmbeddedParseResult, TiledError> {
        let ((), (first_gid, source)) = get_attrs!(
            attrs,
            optionals: [],
            required: [
                ("firstgid", first_gid, |v:String| v.parse().ok().map(|n| Gid(n))),
                ("source", name, |v| Some(v)),
            ],
            TiledError::MalformedAttributes("Tileset reference must have a firstgid and source with correct types".to_string())
        );

        let tileset_path = path_relative_to
            .ok_or(TiledError::SourceRequired {
                object_to_parse: "Tileset".to_string(),
            })?
            .join(source);

        Ok(EmbeddedParseResult {
            first_gid,
            result_type: EmbeddedParseResultType::ExternalReference { tileset_path },
        })
    }

    fn parse_external_tileset(
        parser: &mut impl Iterator<Item = XmlEventResult>,
        attrs: &Vec<OwnedAttribute>,
        path: Option<&Path>,
    ) -> Result<Tileset, TiledError> {
        let ((spacing, margin, columns), (tilecount, name, tile_width, tile_height)) = get_attrs!(
            attrs,
            optionals: [
                ("spacing", spacing, |v:String| v.parse().ok()),
                ("margin", margin, |v:String| v.parse().ok()),
                ("columns", columns, |v:String| v.parse().ok()),
            ],
            required: [
                ("tilecount", tilecount, |v:String| v.parse().ok()),
                ("name", name, |v| Some(v)),
                ("tilewidth", width, |v:String| v.parse().ok()),
                ("tileheight", height, |v:String| v.parse().ok()),
            ],
            TiledError::MalformedAttributes("tileset must have a name, tile width and height with correct types".to_string())
        );

        let source_path = path.and_then(|p| p.parent().map(Path::to_owned));

        Self::finish_parsing_xml(
            parser,
            TilesetProperties {
                spacing,
                margin,
                name,
                path_relative_to: source_path,
                columns,
                tilecount,
                tile_height,
                tile_width,
                source: path.map(Path::to_owned),
            },
        )
    }

    fn finish_parsing_xml(
        parser: &mut impl Iterator<Item = XmlEventResult>,
        prop: TilesetProperties,
    ) -> Result<Tileset, TiledError> {
        let mut image = Option::None;
        let mut tiles = HashMap::with_capacity(prop.tilecount as usize);
        let mut properties = HashMap::new();

        parse_tag!(parser, "tileset", {
            "image" => |attrs| {
                image = Some(Image::new(parser, attrs, prop.path_relative_to.as_ref().ok_or(TiledError::SourceRequired{object_to_parse: "Image".to_string()})?)?);
                Ok(())
            },
            "properties" => |_| {
                properties = parse_properties(parser)?;
                Ok(())
            },
            "tile" => |attrs| {
                let (id, tile) = Tile::new(parser, attrs, prop.path_relative_to.as_ref().and_then(|p| Some(p.as_path())))?;
                tiles.insert(id, tile);
                Ok(())
            },
        });

        // A tileset is considered an image collection tileset if there is no image attribute (because its tiles do).
        let is_image_collection_tileset = image.is_none();

        if !is_image_collection_tileset {
            for tile_id in 0..prop.tilecount {
                tiles.entry(tile_id).or_default();
            }
        }

        let margin = prop.margin.unwrap_or(0);
        let spacing = prop.spacing.unwrap_or(0);
        let columns = prop
            .columns
            .map(Ok)
            .unwrap_or_else(|| Self::calculate_columns(&image, prop.tile_width, margin, spacing))?;

        Ok(Tileset {
            name: prop.name,
            tile_width: prop.tile_width,
            tile_height: prop.tile_height,
            spacing,
            margin,
            columns,
            tilecount: prop.tilecount,
            image,
            tiles,
            properties,
            source: prop.source,
        })
    }

    fn calculate_columns(
        image: &Option<Image>,
        tile_width: u32,
        margin: u32,
        spacing: u32,
    ) -> Result<u32, TiledError> {
        image
            .as_ref()
            .ok_or(TiledError::MalformedAttributes(
                "No <image> nor columns attribute in <tileset>".to_string(),
            ))
            .and_then(|image| Ok((image.width as u32 - margin + spacing) / (tile_width + spacing)))
    }
}