use crate::{ensure_element, ensure_tag_name, get_attribute};
use anyhow::{anyhow, bail, Context};
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>.
pub tileid: u32,
// How long (in milliseconds) this frame should be displayed before advancing to the next frame.
pub 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: i32, // TODO find out why `-1` is allowed here and maybe if there is a better way to represent it in rust?
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 TSX format version.
pub version: Option<String>,
/// 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: Vec<Terrain>,
/// (since 1.1)
pub wangsets: 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(),
};
let mut wangsets = Vec::new();
if let Some(wangsets_node) = node.children().find(|&c| c.has_tag_name("wangsets")) {
for wangset_node in wangsets_node
.children()
.filter(|n| n.has_tag_name("wangset"))
{
wangsets.push(WangSet::from_xml(wangset_node)?);
}
}
let mut terrain_types = Vec::new();
if let Some(terraintypes_node) = node.children().find(|&c| c.has_tag_name("terraintypes")) {
for terrain_node in terraintypes_node
.children()
.filter(|n| n.has_tag_name("terrain"))
{
terrain_types.push(Terrain::from_xml(terrain_node).with_context(|| {
format!("Failed to parse terraintype from:\n{:?}", terrain_node,)
})?);
}
}
let version = if let Ok(v) = get_attribute!(node, "version") {
Some(v.to_owned())
} else {
None
};
let tile_width = get_attribute!(node, "tilewidth")?.parse()?;
let tile_height = get_attribute!(node, "tileheight")?.parse()?;
let tile_count: usize = match get_attribute!(node, "tilecount") {
Ok(tc_string) => tc_string.parse()?,
Err(err) => {
if let Some(img) = &image {
// there is no tilecount, but an image so we can guess/calculate the tilecount
// TODO TEST for this
if spacing != 0 {
bail!("tilecount not set, and calculationg it with spacing enabled is not implemented yet.");
}
if let Some(image_width) = img.width {
if let Some(image_heigth) = img.height {
let real_tile_with = tile_width + (margin * 2);
let real_tile_height = tile_height + (margin * 2);
let columns = image_width / real_tile_with;
let rows = image_heigth / real_tile_height;
columns * rows
} else {
bail!("tilecount not set, calculating failed: image has no height");
}
} else {
bail!("tilecount not set, calculating failed: image has no width");
}
} else {
return Err(err);
}
}
};
let columns: usize = match get_attribute!(node, "columns") {
Ok(c) => c.parse()?,
Err(err) => {
if let Some(img) = &image {
// there is no columns attribute, but an image so we can guess/calculate the columns
// TODO TEST for this
if spacing != 0 {
bail!("columns not set, and calculationg it with spacing enabled is not implemented yet.");
}
if let Some(image_width) = img.width {
if let Some(image_heigth) = img.height {
let real_tile_with = tile_width + (margin * 2);
let real_tile_height = tile_height + (margin * 2);
let columns = image_width / real_tile_with;
let rows = image_heigth / real_tile_height;
columns * rows
} else {
bail!("columns not set, calculating failed: image has no height");
}
} else {
bail!("columns not set, calculating failed: image has no width");
}
} else {
return Err(err);
}
}
};
Ok(TileSet {
version,
name: get_attribute!(node, "name")?.to_owned(),
tile_width,
tile_height,
spacing,
margin,
tile_count,
columns,
object_alignment,
tile: tiles,
image,
tile_offset,
grid,
properties,
terrain_types,
wangsets,
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 {
version: None,
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: vec![],
wangsets: vec![],
transformations: TileSetTransformations::default()
}
);
}
// todo more tests
}
/// represents a tileset element inside a tilemap
#[derive(Debug, PartialEq)]
pub enum TileSetElement {
Embedded { first_gid: usize, tileset: TileSet },
External { first_gid: usize, source: String },
}
impl TileSetElement {
pub fn from_xml(node: roxmltree::Node) -> anyhow::Result<TileSetElement> {
ensure_element!(node);
ensure_tag_name!(node, "tileset");
let first_gid = get_attribute!(node, "firstgid")?.parse()?;
if let Ok(source) = get_attribute!(node, "source") {
Ok(TileSetElement::External {
first_gid,
source: source.to_owned(),
})
} else {
Ok(TileSetElement::Embedded {
first_gid,
tileset: TileSet::from_xml(&node)?,
})
}
}
}
#[cfg(test)]
mod test_tileset_element {
use crate::lowlevel::image::*;
use crate::lowlevel::tileset::*;
#[test]
fn embedded_tilemap() {
crate::parse_property_test!(
TileSetElement,
r##"<tileset firstgid="1" name="hex mini" tilewidth="18" tileheight="18" tilecount="20" columns="5">
<tileoffset x="0" y="1"/>
<image source="hexmini.png" width="106" height="72"/>
</tileset>"##,
TileSetElement::Embedded {
first_gid: 1,
tileset: TileSet {
version: None,
name: "hex mini".to_owned(),
tile_width: 18,
tile_height: 18,
tile_offset: TileOffset { x: 0, y: 1 },
terrain_types: vec![],
wangsets: vec![],
spacing: 0,
margin: 0,
tile_count: 20,
columns: 5,
object_alignment: ObjectAlignment::default(),
tile: vec![],
image: Some(Image {
source: ImageSource::External {
source: "hexmini.png".to_owned()
},
transparent_color: None,
width: Some(106),
height: Some(72)
}),
grid: None,
properties: Properties::default(),
transformations: TileSetTransformations::default(),
}
}
);
}
#[test]
fn external_tilemap() {
crate::parse_property_test!(
TileSetElement,
r##"<tileset firstgid="1" source="beach_tileset.tsx"/>"##,
TileSetElement::External {
first_gid: 1,
source: "beach_tileset.tsx".to_owned()
}
);
}
}