use std::collections::HashMap;
use std::fmt;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use crate::error::{Error, Result};
use crate::image::Image;
use crate::properties::{Properties, parse_properties};
use crate::tile::TileData;
use crate::{
Gid, InvalidTilesetError, ResourceCache, ResourceReader, Tile, TileId,
util::{get_attrs, parse_tag},
};
mod wangset;
pub use wangset::*;
#[derive(Debug, PartialEq, Clone)]
pub struct Tileset {
pub source: PathBuf,
pub name: String,
pub tile_width: u32,
pub tile_height: u32,
pub spacing: u32,
pub margin: u32,
pub tilecount: u32,
pub columns: u32,
pub offset_x: i32,
pub offset_y: i32,
pub tile_render_size: TileRenderSize,
pub fill_mode: FillMode,
pub object_alignment: ObjectAlignment,
pub transformations: Transformations,
pub image: Option<Image>,
tiles: HashMap<TileId, TileData>,
pub wang_sets: Vec<WangSet>,
pub properties: Properties,
pub user_type: String,
}
#[derive(Debug, Default, PartialEq, Eq, Copy, Clone)]
pub struct Transformations {
pub hflip: bool,
pub vflip: bool,
pub rotate: bool,
pub prefer_untransformed: bool,
}
#[derive(Debug, Default, PartialEq, Eq, Copy, Clone)]
pub enum TileRenderSize {
#[default]
Tile,
Grid,
}
#[derive(Debug)]
pub struct TileRenderSizeParseError {
pub str_found: String,
}
impl fmt::Display for TileRenderSizeParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_fmt(format_args!(
"failed to parse tile render size, valid options are `tile` and `grid` \
but got `{}` instead",
self.str_found
))
}
}
impl FromStr for TileRenderSize {
type Err = TileRenderSizeParseError;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
match s {
"tile" => Ok(TileRenderSize::Tile),
"grid" => Ok(TileRenderSize::Grid),
_ => Err(TileRenderSizeParseError {
str_found: s.to_owned(),
}),
}
}
}
impl fmt::Display for TileRenderSize {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
TileRenderSize::Tile => write!(f, "tile"),
TileRenderSize::Grid => write!(f, "grid"),
}
}
}
#[derive(Debug, Default, PartialEq, Eq, Copy, Clone)]
pub enum FillMode {
#[default]
Stretch,
PreserveAspectFit,
}
#[derive(Debug)]
pub struct FillModeParseError {
pub str_found: String,
}
impl fmt::Display for FillModeParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_fmt(format_args!(
"failed to parse fill mode, valid options are `stretch` and `preserve-aspect-fit` \
but got `{}` instead",
self.str_found
))
}
}
impl FromStr for FillMode {
type Err = FillModeParseError;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
match s {
"stretch" => Ok(FillMode::Stretch),
"preserve-aspect-fit" => Ok(FillMode::PreserveAspectFit),
_ => Err(FillModeParseError {
str_found: s.to_owned(),
}),
}
}
}
impl fmt::Display for FillMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
FillMode::Stretch => write!(f, "stretch"),
FillMode::PreserveAspectFit => write!(f, "preserve-aspect-fit"),
}
}
}
#[derive(Debug, Default, PartialEq, Eq, Copy, Clone)]
#[allow(missing_docs)]
pub enum ObjectAlignment {
#[default]
Unspecified,
TopLeft,
Top,
TopRight,
Left,
Center,
Right,
BottomLeft,
Bottom,
BottomRight,
}
#[derive(Debug)]
pub struct ObjectAlignmentParseError {
pub str_found: String,
}
impl fmt::Display for ObjectAlignmentParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_fmt(format_args!(
"failed to parse object alignment, valid options are `unspecified`, `topleft`, \
`top`, `topright`, `left`, `center`, `right`, `bottomleft`, `bottom` and `bottomright` \
but got `{}` instead",
self.str_found
))
}
}
impl FromStr for ObjectAlignment {
type Err = ObjectAlignmentParseError;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
match s {
"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(ObjectAlignmentParseError {
str_found: s.to_owned(),
}),
}
}
}
impl fmt::Display for ObjectAlignment {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ObjectAlignment::Unspecified => write!(f, "unspecified"),
ObjectAlignment::TopLeft => write!(f, "topleft"),
ObjectAlignment::Top => write!(f, "top"),
ObjectAlignment::TopRight => write!(f, "topright"),
ObjectAlignment::Left => write!(f, "left"),
ObjectAlignment::Center => write!(f, "center"),
ObjectAlignment::Right => write!(f, "right"),
ObjectAlignment::BottomLeft => write!(f, "bottomleft"),
ObjectAlignment::Bottom => write!(f, "bottom"),
ObjectAlignment::BottomRight => write!(f, "bottomright"),
}
}
}
pub(crate) enum EmbeddedParseResultType {
ExternalReference { tileset_path: PathBuf },
Embedded { tileset: Box<Tileset> },
}
pub(crate) struct EmbeddedParseResult {
pub first_gid: Gid,
pub result_type: EmbeddedParseResultType,
}
struct TilesetProperties {
spacing: Option<u32>,
margin: Option<u32>,
tilecount: u32,
columns: Option<u32>,
name: String,
user_type: String,
tile_width: u32,
tile_height: u32,
tile_render_size: Option<TileRenderSize>,
fill_mode: Option<FillMode>,
object_alignment: Option<ObjectAlignment>,
root_path: PathBuf,
}
impl Tileset {
#[inline]
pub fn get_tile(&self, id: TileId) -> Option<Tile<'_>> {
self.tiles.get(&id).map(|data| Tile::new(self, data))
}
#[inline]
pub fn tiles(&self) -> impl ExactSizeIterator<Item = (TileId, Tile<'_>)> {
self.tiles
.iter()
.map(move |(id, data)| (*id, Tile::new(self, data)))
}
}
impl Tileset {
pub(crate) fn parse_xml_in_map<R: std::io::BufRead>(
elem: crate::util::XmlElement<'_, R>,
path: &Path, reader: &mut impl ResourceReader,
cache: &mut impl ResourceCache,
) -> Result<EmbeddedParseResult> {
let attrs = elem.attrs.clone();
Tileset::parse_xml_embedded(elem, attrs.clone(), path, reader, cache).or_else(|err| {
if matches!(err, Error::MalformedAttributes(_)) {
Tileset::parse_xml_reference(attrs, path)
} else {
Err(err)
}
})
}
fn parse_xml_embedded<'a, R: std::io::BufRead>(
elem: crate::util::XmlElement<'a, R>,
attrs: quick_xml::events::BytesStart<'a>,
path: &Path, reader: &mut impl ResourceReader,
cache: &mut impl ResourceCache,
) -> Result<EmbeddedParseResult> {
let (
(spacing, margin, columns, name, user_type, user_class),
(tile_render_size, fill_mode, object_alignment),
(tilecount, first_gid, tile_width, tile_height),
) = get_attrs!(
for v in (attrs) {
Some("spacing") => spacing ?= v.parse(),
Some("margin") => margin ?= v.parse(),
Some("columns") => columns ?= v.parse(),
Some("name") => name = v.to_string(),
Some("type") => user_type ?= v.parse(),
Some("class") => user_class ?= v.parse(),
Some("tilerendersize") => tile_render_size ?= v.parse::<TileRenderSize>(),
Some("fillmode") => fill_mode ?= v.parse::<FillMode>(),
Some("objectalignment") => object_alignment ?= v.parse::<ObjectAlignment>(),
"tilecount" => tilecount ?= v.parse::<u32>(),
"firstgid" => first_gid ?= v.parse::<u32>().map(Gid),
"tilewidth" => tile_width ?= v.parse::<u32>(),
"tileheight" => tile_height ?= v.parse::<u32>(),
}
((spacing, margin, columns, name, user_type, user_class), (tile_render_size, fill_mode, object_alignment), (tilecount, first_gid, tile_width, tile_height))
);
let root_path = path.parent().ok_or(Error::PathIsNotFile)?.to_owned();
Self::finish_parsing_xml(
elem,
path.to_owned(),
TilesetProperties {
spacing,
margin,
name: name.unwrap_or_default(),
user_type: user_type.or(user_class).unwrap_or_default(),
root_path,
columns,
tilecount,
tile_height,
tile_width,
tile_render_size,
fill_mode,
object_alignment,
},
reader,
cache,
)
.map(|tileset| EmbeddedParseResult {
first_gid,
result_type: EmbeddedParseResultType::Embedded {
tileset: Box::new(tileset),
},
})
}
fn parse_xml_reference<'a>(
attrs: quick_xml::events::BytesStart<'a>,
map_path: &Path,
) -> Result<EmbeddedParseResult> {
let (first_gid, source) = get_attrs!(
for v in (attrs) {
"firstgid" => first_gid ?= v.parse::<u32>().map(Gid),
"source" => source = v.to_string(),
}
(first_gid, source)
);
let tileset_path = map_path.parent().ok_or(Error::PathIsNotFile)?.join(source);
Ok(EmbeddedParseResult {
first_gid,
result_type: EmbeddedParseResultType::ExternalReference { tileset_path },
})
}
pub(crate) fn parse_external_tileset<R: std::io::BufRead>(
elem: crate::util::XmlElement<'_, R>,
path: &Path,
reader: &mut impl ResourceReader,
cache: &mut impl ResourceCache,
) -> Result<Tileset> {
let (
(spacing, margin, columns, name, user_type, user_class),
(tile_render_size, fill_mode, object_alignment),
(tilecount, tile_width, tile_height),
) = get_attrs!(
for v in (elem.attrs) {
Some("spacing") => spacing ?= v.parse(),
Some("margin") => margin ?= v.parse(),
Some("columns") => columns ?= v.parse(),
Some("name") => name = v.to_string(),
Some("type") => user_type ?= v.parse(),
Some("class") => user_class ?= v.parse(),
Some("tilerendersize") => tile_render_size ?= v.parse::<TileRenderSize>(),
Some("fillmode") => fill_mode ?= v.parse::<FillMode>(),
Some("objectalignment") => object_alignment ?= v.parse::<ObjectAlignment>(),
"tilecount" => tilecount ?= v.parse::<u32>(),
"tilewidth" => tile_width ?= v.parse::<u32>(),
"tileheight" => tile_height ?= v.parse::<u32>(),
}
((spacing, margin, columns, name, user_type, user_class), (tile_render_size, fill_mode, object_alignment), (tilecount, tile_width, tile_height))
);
let root_path = path.parent().ok_or(Error::PathIsNotFile)?.to_owned();
Self::finish_parsing_xml(
elem,
path.to_owned(),
TilesetProperties {
spacing,
margin,
name: name.unwrap_or_default(),
user_type: user_type.or(user_class).unwrap_or_default(),
root_path,
columns,
tilecount,
tile_height,
tile_width,
tile_render_size,
fill_mode,
object_alignment,
},
reader,
cache,
)
}
fn finish_parsing_xml<R: std::io::BufRead>(
elem: crate::util::XmlElement<'_, R>,
container_path: PathBuf,
prop: TilesetProperties,
reader: &mut impl ResourceReader,
cache: &mut impl ResourceCache,
) -> Result<Tileset> {
let mut image = Option::None;
let mut tiles = HashMap::with_capacity(prop.tilecount as usize);
let mut properties = HashMap::new();
let mut wang_sets = Vec::new();
let mut offset = (0i32, 0i32);
let mut transformations = Transformations::default();
parse_tag!(elem, {
"image" => |elem| {
image = Some(Image::new(elem, &prop.root_path)?);
Ok(())
},
"tileoffset" => |elem| {
offset = parse_tileoffset(elem)?;
Ok(())
},
"transformations" => |elem| {
transformations = parse_transformations(elem)?;
Ok(())
},
"properties" => |elem| {
properties = parse_properties(elem)?;
Ok(())
},
"tile" => |elem| {
let (id, tile) = TileData::new(elem, &prop.root_path, reader, cache)?;
tiles.insert(id, tile);
Ok(())
},
"wangsets" => |elem: crate::util::XmlElement<'_, R>| {
parse_tag!(elem, {
"wangset" => |elem| {
let set = WangSet::new(elem)?;
wang_sets.push(set);
Ok(())
},
});
Ok(())
},
});
let is_image_collection_tileset = image.is_none();
if !is_image_collection_tileset {
if prop.tile_width == 0 || prop.tile_height == 0 {
return Err(Error::InvalidTileset(
InvalidTilesetError::InvalidTileDimensions,
));
}
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 {
source: container_path,
name: prop.name,
user_type: prop.user_type,
tile_width: prop.tile_width,
tile_height: prop.tile_height,
spacing,
margin,
columns,
offset_x: offset.0,
offset_y: offset.1,
tile_render_size: prop.tile_render_size.unwrap_or_default(),
fill_mode: prop.fill_mode.unwrap_or_default(),
object_alignment: prop.object_alignment.unwrap_or_default(),
transformations,
tilecount: prop.tilecount,
image,
tiles,
wang_sets,
properties,
})
}
fn calculate_columns(
image: &Option<Image>,
tile_width: u32,
margin: u32,
spacing: u32,
) -> Result<u32> {
image
.as_ref()
.map(|image| (image.width as u32 - margin + spacing) / (tile_width + spacing))
.ok_or_else(|| {
Error::MalformedAttributes(
"No <image> nor columns attribute in <tileset>".to_string(),
)
})
}
}
fn parse_transformations<R: std::io::BufRead>(
elem: crate::util::XmlElement<'_, R>,
) -> Result<Transformations> {
let (hflip, vflip, rotate, prefer_untransformed) = get_attrs!(
for v in (elem.attrs) {
Some("hflip") => hflip = v == "1",
Some("vflip") => vflip = v == "1",
Some("rotate") => rotate = v == "1",
Some("preferuntransformed") => prefer_untransformed = v == "1",
}
(hflip, vflip, rotate, prefer_untransformed)
);
parse_tag!(elem, {});
Ok(Transformations {
hflip: hflip.unwrap_or(false),
vflip: vflip.unwrap_or(false),
rotate: rotate.unwrap_or(false),
prefer_untransformed: prefer_untransformed.unwrap_or(false),
})
}
fn parse_tileoffset<R: std::io::BufRead>(
elem: crate::util::XmlElement<'_, R>,
) -> Result<(i32, i32)> {
let offset = get_attrs!(
for v in (elem.attrs) {
"x" => offset_x ?= v.parse::<i32>(),
"y" => offset_y ?= v.parse::<i32>(),
}
(offset_x, offset_y)
);
parse_tag!(elem, {});
Ok(offset)
}