Merge pull request #48 from benjamin-kirkbride/2.0

2.0
This commit is contained in:
Darren Eberly
2021-12-21 22:14:02 -05:00
committed by GitHub
76 changed files with 3787 additions and 1288 deletions

View File

@@ -12,8 +12,10 @@ PyTiled Parser is not tied to any particular graphics library or game engine.
# pylint: disable=too-few-public-methods
from .common_types import OrderedPair, Size
from .exception import UnknownFormat
from .layer import ImageLayer, Layer, LayerGroup, ObjectLayer, TileLayer
from .parser import parse_map
from .properties import Properties
from .tiled_map import TiledMap, parse_map
from .tiled_map import TiledMap
from .tileset import Tile, Tileset
from .version import __version__

View File

@@ -0,0 +1,2 @@
class UnknownFormat(Exception):
pass

View File

@@ -8,27 +8,14 @@ See:
# pylint: disable=too-few-public-methods
import base64
import gzip
import importlib.util
import zlib
from pathlib import Path
from typing import Any, List, Optional, Union
from typing import cast as type_cast
from typing import List, Optional, Union
import attr
from typing_extensions import TypedDict
from . import properties as properties_
from . import tiled_object
from .common_types import Color, OrderedPair, Size
from .util import parse_color
zstd_spec = importlib.util.find_spec("zstd")
if zstd_spec:
import zstd # pylint: disable=import-outside-toplevel
else:
zstd = None # pylint: disable=invalid-name
from pytiled_parser.common_types import Color, OrderedPair, Size
from pytiled_parser.properties import Properties
from pytiled_parser.tiled_object import TiledObject
@attr.s(auto_attribs=True, kw_only=True)
@@ -51,8 +38,8 @@ class Layer:
"""
name: str
opacity: float
visible: bool
opacity: float = 1
visible: bool = True
coordinates: OrderedPair = OrderedPair(0, 0)
parallax_factor: OrderedPair = OrderedPair(1, 1)
@@ -60,7 +47,7 @@ class Layer:
id: Optional[int] = None
size: Optional[Size] = None
properties: Optional[properties_.Properties] = None
properties: Optional[Properties] = None
tint_color: Optional[Color] = None
@@ -127,7 +114,7 @@ class ObjectLayer(Layer):
for more info.
"""
tiled_objects: List[tiled_object.TiledObject]
tiled_objects: List[TiledObject]
draw_order: Optional[str] = "topdown"
@@ -162,341 +149,3 @@ class LayerGroup(Layer):
"""
layers: Optional[List[Layer]]
class RawChunk(TypedDict):
"""The keys and their types that appear in a Chunk JSON Object.
See: https://doc.mapeditor.org/en/stable/reference/json-map-format/#chunk
"""
data: Union[List[int], str]
height: int
width: int
x: int
y: int
class RawLayer(TypedDict):
"""The keys and their types that appear in a Layer JSON Object.
See: https://doc.mapeditor.org/en/stable/reference/json-map-format/#layer
"""
chunks: List[RawChunk]
compression: str
data: Union[List[int], str]
draworder: str
encoding: str
height: int
id: int
image: str
layers: List[Any]
name: str
objects: List[tiled_object.RawTiledObject]
offsetx: float
offsety: float
parallaxx: float
parallaxy: float
opacity: float
properties: List[properties_.RawProperty]
startx: int
starty: int
tintcolor: str
transparentcolor: str
type: str
visible: bool
width: int
x: int
y: int
def _convert_raw_tile_layer_data(data: List[int], layer_width: int) -> List[List[int]]:
"""Convert raw layer data into a nested lit based on the layer width
Args:
data: The data to convert
layer_width: Width of the layer
Returns:
List[List[int]]: A nested list containing the converted data
"""
tile_grid: List[List[int]] = [[]]
column_count = 0
row_count = 0
for item in data:
column_count += 1
tile_grid[row_count].append(item)
if not column_count % layer_width and column_count < len(data):
row_count += 1
tile_grid.append([])
return tile_grid
def _decode_tile_layer_data(
data: str, compression: str, layer_width: int
) -> List[List[int]]:
"""Decode Base64 Encoded tile data. Optionally supports gzip and zlib compression.
Args:
data: The base64 encoded data
compression: Either zlib, gzip, or empty. If empty no decompression is done.
Returns:
List[List[int]]: A nested list containing the decoded data
Raises:
ValueError: For an unsupported compression type.
"""
unencoded_data = base64.b64decode(data)
if compression == "zlib":
unzipped_data = zlib.decompress(unencoded_data)
elif compression == "gzip":
unzipped_data = gzip.decompress(unencoded_data)
elif compression == "zstd" and zstd is None:
raise ValueError(
"zstd compression support is not installed."
"To install use 'pip install pytiled-parser[zstd]'"
)
elif compression == "zstd":
unzipped_data = zstd.decompress(unencoded_data)
else:
unzipped_data = unencoded_data
tile_grid: List[int] = []
byte_count = 0
int_count = 0
int_value = 0
for byte in unzipped_data:
int_value += byte << (byte_count * 8)
byte_count += 1
if not byte_count % 4:
byte_count = 0
int_count += 1
tile_grid.append(int_value)
int_value = 0
return _convert_raw_tile_layer_data(tile_grid, layer_width)
def _cast_chunk(
raw_chunk: RawChunk,
encoding: Optional[str] = None,
compression: Optional[str] = None,
) -> Chunk:
"""Cast the raw_chunk to a Chunk.
Args:
raw_chunk: RawChunk to be casted to a Chunk
encoding: Encoding type. ("base64" or None)
compression: Either zlib, gzip, or empty. If empty no decompression is done.
Returns:
Chunk: The Chunk created from the raw_chunk
"""
if encoding == "base64":
assert isinstance(compression, str)
assert isinstance(raw_chunk["data"], str)
data = _decode_tile_layer_data(
raw_chunk["data"], compression, raw_chunk["width"]
)
else:
data = _convert_raw_tile_layer_data(
raw_chunk["data"], raw_chunk["width"] # type: ignore
)
chunk = Chunk(
coordinates=OrderedPair(raw_chunk["x"], raw_chunk["y"]),
size=Size(raw_chunk["width"], raw_chunk["height"]),
data=data,
)
return chunk
def _get_common_attributes(raw_layer: RawLayer) -> Layer:
"""Create a Layer containing all the attributes common to all layers.
This is to create the stub Layer object that can then be used to create the actual
specific sub-classes of Layer.
Args:
raw_layer: Raw Tiled object get common attributes from
Returns:
Layer: The attributes in common of all layers
"""
common_attributes = Layer(
name=raw_layer["name"],
opacity=raw_layer["opacity"],
visible=raw_layer["visible"],
)
# if startx is present, starty is present
if raw_layer.get("startx") is not None:
common_attributes.coordinates = OrderedPair(
raw_layer["startx"], raw_layer["starty"]
)
if raw_layer.get("id") is not None:
common_attributes.id = raw_layer["id"]
# if either width or height is present, they both are
if raw_layer.get("width") is not None:
common_attributes.size = Size(raw_layer["width"], raw_layer["height"])
if raw_layer.get("offsetx") is not None:
common_attributes.offset = OrderedPair(
raw_layer["offsetx"], raw_layer["offsety"]
)
if raw_layer.get("properties") is not None:
common_attributes.properties = properties_.cast(raw_layer["properties"])
parallax = [1.0, 1.0]
if raw_layer.get("parallaxx") is not None:
parallax[0] = raw_layer["parallaxx"]
if raw_layer.get("parallaxy") is not None:
parallax[1] = raw_layer["parallaxy"]
common_attributes.parallax_factor = OrderedPair(parallax[0], parallax[1])
if raw_layer.get("tintcolor") is not None:
common_attributes.tint_color = parse_color(raw_layer["tintcolor"])
return common_attributes
def _cast_tile_layer(raw_layer: RawLayer) -> TileLayer:
"""Cast the raw_layer to a TileLayer.
Args:
raw_layer: RawLayer to be casted to a TileLayer
Returns:
TileLayer: The TileLayer created from raw_layer
"""
tile_layer = TileLayer(**_get_common_attributes(raw_layer).__dict__)
if raw_layer.get("chunks") is not None:
tile_layer.chunks = []
for chunk in raw_layer["chunks"]:
if raw_layer.get("encoding") is not None:
tile_layer.chunks.append(
_cast_chunk(chunk, raw_layer["encoding"], raw_layer["compression"])
)
else:
tile_layer.chunks.append(_cast_chunk(chunk))
if raw_layer.get("data") is not None:
if raw_layer.get("encoding") is not None:
tile_layer.data = _decode_tile_layer_data(
data=type_cast(str, raw_layer["data"]),
compression=raw_layer["compression"],
layer_width=raw_layer["width"],
)
else:
tile_layer.data = _convert_raw_tile_layer_data(
raw_layer["data"], raw_layer["width"] # type: ignore
)
return tile_layer
def _cast_object_layer(
raw_layer: RawLayer,
parent_dir: Optional[Path] = None,
) -> ObjectLayer:
"""Cast the raw_layer to an ObjectLayer.
Args:
raw_layer: RawLayer to be casted to an ObjectLayer
Returns:
ObjectLayer: The ObjectLayer created from raw_layer
"""
tiled_objects = []
for tiled_object_ in raw_layer["objects"]:
tiled_objects.append(tiled_object.cast(tiled_object_, parent_dir))
return ObjectLayer(
tiled_objects=tiled_objects,
draw_order=raw_layer["draworder"],
**_get_common_attributes(raw_layer).__dict__,
)
def _cast_image_layer(raw_layer: RawLayer) -> ImageLayer:
"""Cast the raw_layer to a ImageLayer.
Args:
raw_layer: RawLayer to be casted to a ImageLayer
Returns:
ImageLayer: The ImageLayer created from raw_layer
"""
image_layer = ImageLayer(
image=Path(raw_layer["image"]), **_get_common_attributes(raw_layer).__dict__
)
if raw_layer.get("transparentcolor") is not None:
image_layer.transparent_color = parse_color(raw_layer["transparentcolor"])
return image_layer
def _cast_group_layer(
raw_layer: RawLayer, parent_dir: Optional[Path] = None
) -> LayerGroup:
"""Cast the raw_layer to a LayerGroup.
Args:
raw_layer: RawLayer to be casted to a LayerGroup
Returns:
LayerGroup: The LayerGroup created from raw_layer
"""
layers = []
for layer in raw_layer["layers"]:
layers.append(cast(layer, parent_dir=parent_dir))
return LayerGroup(layers=layers, **_get_common_attributes(raw_layer).__dict__)
def cast(
raw_layer: RawLayer,
parent_dir: Optional[Path] = None,
) -> Layer:
"""Cast a raw Tiled layer into a pytiled_parser type.
This function will determine the type of layer and cast accordingly.
Args:
raw_layer: Raw layer to be cast.
parent_dir: The parent directory that the map file is in.
Returns:
Layer: a properly typed Layer.
Raises:
RuntimeError: For an invalid layer type being provided
"""
type_ = raw_layer["type"]
if type_ == "objectgroup":
return _cast_object_layer(raw_layer, parent_dir)
elif type_ == "group":
return _cast_group_layer(raw_layer, parent_dir)
elif type_ == "imagelayer":
return _cast_image_layer(raw_layer)
elif type_ == "tilelayer":
return _cast_tile_layer(raw_layer)
raise RuntimeError(f"An invalid layer type of {type_} was supplied")

29
pytiled_parser/parser.py Normal file
View File

@@ -0,0 +1,29 @@
from pathlib import Path
from pytiled_parser import UnknownFormat
from pytiled_parser.parsers.json.tiled_map import parse as json_map_parse
from pytiled_parser.parsers.tmx.tiled_map import parse as tmx_map_parse
from pytiled_parser.tiled_map import TiledMap
from pytiled_parser.util import check_format
def parse_map(file: Path) -> TiledMap:
"""Parse the raw Tiled map into a pytiled_parser type
Args:
file: Path to the map file
Returns:
Tiledmap: a properly typed TiledMap
"""
parser = check_format(file)
# The type ignores are because mypy for some reaosn thinks those functions return Any
if parser == "tmx":
return tmx_map_parse(file) # type: ignore
elif parser == "json":
return json_map_parse(file) # type: ignore
else:
raise UnknownFormat(
"Unknown Map Format, please use either the TMX or JSON format."
)

View File

@@ -0,0 +1,364 @@
"""Layer parsing for the JSON Map Format.
"""
import base64
import gzip
import importlib.util
import zlib
from pathlib import Path
from typing import Any, List, Optional, Union, cast
from typing_extensions import TypedDict
from pytiled_parser.common_types import OrderedPair, Size
from pytiled_parser.layer import (
Chunk,
ImageLayer,
Layer,
LayerGroup,
ObjectLayer,
TileLayer,
)
from pytiled_parser.parsers.json.properties import RawProperty
from pytiled_parser.parsers.json.properties import parse as parse_properties
from pytiled_parser.parsers.json.tiled_object import RawObject
from pytiled_parser.parsers.json.tiled_object import parse as parse_object
from pytiled_parser.util import parse_color
zstd_spec = importlib.util.find_spec("zstd")
if zstd_spec:
import zstd
else:
zstd = None
class RawChunk(TypedDict):
"""The keys and their types that appear in a Tiled JSON Chunk Object.
Tiled Doc: https://doc.mapeditor.org/en/stable/reference/json-map-format/#chunk
"""
data: Union[List[int], str]
height: int
width: int
x: int
y: int
class RawLayer(TypedDict):
"""The keys and their types that appear in a Tiled JSON Layer Object.
Tiled Doc: https://doc.mapeditor.org/en/stable/reference/json-map-format/#layer
"""
chunks: List[RawChunk]
compression: str
data: Union[List[int], str]
draworder: str
encoding: str
height: int
id: int
image: str
layers: List[Any]
name: str
objects: List[RawObject]
offsetx: float
offsety: float
parallaxx: float
parallaxy: float
opacity: float
properties: List[RawProperty]
startx: int
starty: int
tintcolor: str
transparentcolor: str
type: str
visible: bool
width: int
x: int
y: int
def _convert_raw_tile_layer_data(data: List[int], layer_width: int) -> List[List[int]]:
"""Convert raw layer data into a nested lit based on the layer width
Args:
data: The data to convert
layer_width: Width of the layer
Returns:
List[List[int]]: A nested list containing the converted data
"""
tile_grid: List[List[int]] = [[]]
column_count = 0
row_count = 0
for item in data:
column_count += 1
tile_grid[row_count].append(item)
if not column_count % layer_width and column_count < len(data):
row_count += 1
tile_grid.append([])
return tile_grid
def _decode_tile_layer_data(
data: str, compression: str, layer_width: int
) -> List[List[int]]:
"""Decode Base64 Encoded tile data. Optionally supports gzip and zlib compression.
Args:
data: The base64 encoded data
compression: Either zlib, gzip, or empty. If empty no decompression is done.
Returns:
List[List[int]]: A nested list containing the decoded data
Raises:
ValueError: For an unsupported compression type.
"""
unencoded_data = base64.b64decode(data)
if compression == "zlib":
unzipped_data = zlib.decompress(unencoded_data)
elif compression == "gzip":
unzipped_data = gzip.decompress(unencoded_data)
elif compression == "zstd" and zstd is None:
raise ValueError(
"zstd compression support is not installed."
"To install use 'pip install pytiled-parser[zstd]'"
)
elif compression == "zstd":
unzipped_data = zstd.decompress(unencoded_data)
else:
unzipped_data = unencoded_data
tile_grid: List[int] = []
byte_count = 0
int_count = 0
int_value = 0
for byte in unzipped_data:
int_value += byte << (byte_count * 8)
byte_count += 1
if not byte_count % 4:
byte_count = 0
int_count += 1
tile_grid.append(int_value)
int_value = 0
return _convert_raw_tile_layer_data(tile_grid, layer_width)
def _parse_chunk(
raw_chunk: RawChunk,
encoding: Optional[str] = None,
compression: Optional[str] = None,
) -> Chunk:
"""Parse the raw_chunk to a Chunk.
Args:
raw_chunk: RawChunk to be parsed to a Chunk
encoding: Encoding type. ("base64" or None)
compression: Either zlib, gzip, or empty. If empty no decompression is done.
Returns:
Chunk: The Chunk created from the raw_chunk
"""
if encoding == "base64":
assert isinstance(compression, str)
assert isinstance(raw_chunk["data"], str)
data = _decode_tile_layer_data(
raw_chunk["data"], compression, raw_chunk["width"]
)
else:
data = _convert_raw_tile_layer_data(
raw_chunk["data"], raw_chunk["width"] # type: ignore
)
chunk = Chunk(
coordinates=OrderedPair(raw_chunk["x"], raw_chunk["y"]),
size=Size(raw_chunk["width"], raw_chunk["height"]),
data=data,
)
return chunk
def _parse_common(raw_layer: RawLayer) -> Layer:
"""Create a Layer containing all the attributes common to all layer types.
This is to create the stub Layer object that can then be used to create the actual
specific sub-classes of Layer.
Args:
raw_layer: Raw layer get common attributes from
Returns:
Layer: The attributes in common of all layer types
"""
common = Layer(
name=raw_layer["name"],
opacity=raw_layer["opacity"],
visible=raw_layer["visible"],
)
# if startx is present, starty is present
if raw_layer.get("startx") is not None:
common.coordinates = OrderedPair(raw_layer["startx"], raw_layer["starty"])
if raw_layer.get("id") is not None:
common.id = raw_layer["id"]
# if either width or height is present, they both are
if raw_layer.get("width") is not None:
common.size = Size(raw_layer["width"], raw_layer["height"])
if raw_layer.get("offsetx") is not None:
common.offset = OrderedPair(raw_layer["offsetx"], raw_layer["offsety"])
if raw_layer.get("properties") is not None:
common.properties = parse_properties(raw_layer["properties"])
parallax = [1.0, 1.0]
if raw_layer.get("parallaxx") is not None:
parallax[0] = raw_layer["parallaxx"]
if raw_layer.get("parallaxy") is not None:
parallax[1] = raw_layer["parallaxy"]
common.parallax_factor = OrderedPair(parallax[0], parallax[1])
if raw_layer.get("tintcolor") is not None:
common.tint_color = parse_color(raw_layer["tintcolor"])
return common
def _parse_tile_layer(raw_layer: RawLayer) -> TileLayer:
"""Parse the raw_layer to a TileLayer.
Args:
raw_layer: RawLayer to be parsed to a TileLayer.
Returns:
TileLayer: The TileLayer created from raw_layer
"""
tile_layer = TileLayer(**_parse_common(raw_layer).__dict__)
if raw_layer.get("chunks") is not None:
tile_layer.chunks = []
for chunk in raw_layer["chunks"]:
if raw_layer.get("encoding") is not None:
tile_layer.chunks.append(
_parse_chunk(chunk, raw_layer["encoding"], raw_layer["compression"])
)
else:
tile_layer.chunks.append(_parse_chunk(chunk))
if raw_layer.get("data") is not None:
if raw_layer.get("encoding") is not None:
tile_layer.data = _decode_tile_layer_data(
data=cast(str, raw_layer["data"]),
compression=raw_layer["compression"],
layer_width=raw_layer["width"],
)
else:
tile_layer.data = _convert_raw_tile_layer_data(
raw_layer["data"], raw_layer["width"] # type: ignore
)
return tile_layer
def _parse_object_layer(
raw_layer: RawLayer,
parent_dir: Optional[Path] = None,
) -> ObjectLayer:
"""Parse the raw_layer to an ObjectLayer.
Args:
raw_layer: RawLayer to be parsed to an ObjectLayer.
Returns:
ObjectLayer: The ObjectLayer created from raw_layer
"""
objects = []
for object_ in raw_layer["objects"]:
objects.append(parse_object(object_, parent_dir))
return ObjectLayer(
tiled_objects=objects,
draw_order=raw_layer["draworder"],
**_parse_common(raw_layer).__dict__,
)
def _parse_image_layer(raw_layer: RawLayer) -> ImageLayer:
"""Parse the raw_layer to an ImageLayer.
Args:
raw_layer: RawLayer to be parsed to an ImageLayer.
Returns:
ImageLayer: The ImageLayer created from raw_layer
"""
image_layer = ImageLayer(
image=Path(raw_layer["image"]), **_parse_common(raw_layer).__dict__
)
if raw_layer.get("transparentcolor") is not None:
image_layer.transparent_color = parse_color(raw_layer["transparentcolor"])
return image_layer
def _parse_group_layer(
raw_layer: RawLayer, parent_dir: Optional[Path] = None
) -> LayerGroup:
"""Parse the raw_layer to a LayerGroup.
Args:
raw_layer: RawLayer to be parsed to a LayerGroup.
Returns:
LayerGroup: The LayerGroup created from raw_layer
"""
layers = []
for layer in raw_layer["layers"]:
layers.append(parse(layer, parent_dir=parent_dir))
return LayerGroup(layers=layers, **_parse_common(raw_layer).__dict__)
def parse(
raw_layer: RawLayer,
parent_dir: Optional[Path] = None,
) -> Layer:
"""Parse a raw Layer into a pytiled_parser object.
This function will determine the type of layer and parse accordingly.
Args:
raw_layer: Raw layer to be parsed.
parent_dir: The parent directory that the map file is in.
Returns:
Layer: A parsed Layer.
Raises:
RuntimeError: For an invalid layer type being provided
"""
type_ = raw_layer["type"]
if type_ == "objectgroup":
return _parse_object_layer(raw_layer, parent_dir)
elif type_ == "group":
return _parse_group_layer(raw_layer, parent_dir)
elif type_ == "imagelayer":
return _parse_image_layer(raw_layer)
elif type_ == "tilelayer":
return _parse_tile_layer(raw_layer)
raise RuntimeError(f"An invalid layer type of {type_} was supplied")

View File

@@ -0,0 +1,48 @@
"""Property parsing for the JSON Map Format
"""
from pathlib import Path
from typing import List, Union, cast
from typing_extensions import TypedDict
from pytiled_parser.properties import Properties, Property
from pytiled_parser.util import parse_color
RawValue = Union[float, str, bool]
class RawProperty(TypedDict):
"""The keys and their values that appear in a Tiled JSON Property Object.
Tiled Docs: https://doc.mapeditor.org/en/stable/reference/json-map-format/#property
"""
name: str
type: str
value: RawValue
def parse(raw_properties: List[RawProperty]) -> Properties:
"""Parse a list of `RawProperty` objects into `Properties`.
Args:
raw_properties: The list of `RawProperty` objects to parse.
Returns:
Properties: The parsed `Property` objects.
"""
final: Properties = {}
value: Property
for raw_property in raw_properties:
if raw_property["type"] == "file":
value = Path(cast(str, raw_property["value"]))
elif raw_property["type"] == "color":
value = parse_color(cast(str, raw_property["value"]))
else:
value = raw_property["value"]
final[raw_property["name"]] = value
return final

View File

@@ -0,0 +1,170 @@
import json
import xml.etree.ElementTree as etree
from pathlib import Path
from typing import List, Union, cast
from typing_extensions import TypedDict
from pytiled_parser.common_types import Size
from pytiled_parser.exception import UnknownFormat
from pytiled_parser.parsers.json.layer import RawLayer
from pytiled_parser.parsers.json.layer import parse as parse_layer
from pytiled_parser.parsers.json.properties import RawProperty
from pytiled_parser.parsers.json.properties import parse as parse_properties
from pytiled_parser.parsers.json.tileset import RawTileSet
from pytiled_parser.parsers.json.tileset import parse as parse_json_tileset
from pytiled_parser.parsers.tmx.tileset import parse as parse_tmx_tileset
from pytiled_parser.tiled_map import TiledMap, TilesetDict
from pytiled_parser.util import check_format, parse_color
class RawTilesetMapping(TypedDict):
firstgid: int
source: str
class RawTiledMap(TypedDict):
"""The keys and their types that appear in a Tiled JSON Map Object.
Tiled Docs: https://doc.mapeditor.org/en/stable/reference/json-map-format/#map
"""
backgroundcolor: str
compressionlevel: int
height: int
hexsidelength: int
infinite: bool
layers: List[RawLayer]
nextlayerid: int
nextobjectid: int
orientation: str
properties: List[RawProperty]
renderorder: str
staggeraxis: str
staggerindex: str
tiledversion: str
tileheight: int
tilesets: List[RawTilesetMapping]
tilewidth: int
type: str
version: Union[str, float]
width: int
def parse(file: Path) -> TiledMap:
"""Parse the raw Tiled map into a pytiled_parser type.
Args:
file: Path to the map file.
Returns:
TiledMap: A parsed TiledMap.
"""
with open(file) as map_file:
raw_tiled_map = json.load(map_file)
parent_dir = file.parent
raw_tilesets: List[Union[RawTileSet, RawTilesetMapping]] = raw_tiled_map["tilesets"]
tilesets: TilesetDict = {}
for raw_tileset in raw_tilesets:
if raw_tileset.get("source") is not None:
# Is an external Tileset
tileset_path = Path(parent_dir / raw_tileset["source"])
parser = check_format(tileset_path)
with open(tileset_path) as raw_tileset_file:
if parser == "json":
tilesets[raw_tileset["firstgid"]] = parse_json_tileset(
json.load(raw_tileset_file),
raw_tileset["firstgid"],
external_path=tileset_path.parent,
)
elif parser == "tmx":
raw_tileset_external = etree.parse(raw_tileset_file).getroot()
tilesets[raw_tileset["firstgid"]] = parse_tmx_tileset(
raw_tileset_external,
raw_tileset["firstgid"],
external_path=tileset_path.parent,
)
else:
raise UnknownFormat(
"Unkown Tileset format, please use either the TSX or JSON format."
)
else:
# Is an embedded Tileset
raw_tileset = cast(RawTileSet, raw_tileset)
tilesets[raw_tileset["firstgid"]] = parse_json_tileset(
raw_tileset, raw_tileset["firstgid"]
)
if isinstance(raw_tiled_map["version"], float):
version = str(raw_tiled_map["version"])
else:
version = raw_tiled_map["version"]
# `map` is a built-in function
map_ = TiledMap(
map_file=file,
infinite=raw_tiled_map["infinite"],
layers=[parse_layer(layer_, parent_dir) for layer_ in raw_tiled_map["layers"]],
map_size=Size(raw_tiled_map["width"], raw_tiled_map["height"]),
next_layer_id=raw_tiled_map["nextlayerid"],
next_object_id=raw_tiled_map["nextobjectid"],
orientation=raw_tiled_map["orientation"],
render_order=raw_tiled_map["renderorder"],
tiled_version=raw_tiled_map["tiledversion"],
tile_size=Size(raw_tiled_map["tilewidth"], raw_tiled_map["tileheight"]),
tilesets=tilesets,
version=version,
)
layers = [layer for layer in map_.layers if hasattr(layer, "tiled_objects")]
for my_layer in layers:
for tiled_object in my_layer.tiled_objects: # type: ignore
if hasattr(tiled_object, "new_tileset"):
if tiled_object.new_tileset:
already_loaded = None
for val in map_.tilesets.values():
if val.name == tiled_object.new_tileset["name"]:
already_loaded = val
break
if not already_loaded:
highest_firstgid = max(map_.tilesets.keys())
last_tileset_count = map_.tilesets[highest_firstgid].tile_count
new_firstgid = highest_firstgid + last_tileset_count
map_.tilesets[new_firstgid] = parse_json_tileset(
tiled_object.new_tileset,
new_firstgid,
tiled_object.new_tileset_path,
)
tiled_object.gid = tiled_object.gid + (new_firstgid - 1)
else:
tiled_object.gid = tiled_object.gid + (
already_loaded.firstgid - 1
)
tiled_object.new_tileset = None
tiled_object.new_tileset_path = None
if raw_tiled_map.get("backgroundcolor") is not None:
map_.background_color = parse_color(raw_tiled_map["backgroundcolor"])
if raw_tiled_map.get("hexsidelength") is not None:
map_.hex_side_length = raw_tiled_map["hexsidelength"]
if raw_tiled_map.get("properties") is not None:
map_.properties = parse_properties(raw_tiled_map["properties"])
if raw_tiled_map.get("staggeraxis") is not None:
map_.stagger_axis = raw_tiled_map["staggeraxis"]
if raw_tiled_map.get("staggerindex") is not None:
map_.stagger_index = raw_tiled_map["staggerindex"]
return map_

View File

@@ -0,0 +1,321 @@
"""Object parsing for the JSON Map Format.
"""
import json
import xml.etree.ElementTree as etree
from pathlib import Path
from typing import Any, Callable, Dict, List, Optional
from typing_extensions import TypedDict
from pytiled_parser.common_types import OrderedPair, Size
from pytiled_parser.parsers.json.properties import RawProperty
from pytiled_parser.parsers.json.properties import parse as parse_properties
from pytiled_parser.tiled_object import (
Ellipse,
Point,
Polygon,
Polyline,
Rectangle,
Text,
Tile,
TiledObject,
)
from pytiled_parser.util import load_object_template, parse_color
class RawText(TypedDict):
"""The keys and their types that appear in a Tiled JSON Text Object.
Tiled Doc: https://doc.mapeditor.org/en/stable/reference/json-map-format/#text-example
"""
text: str
color: str
fontfamily: str
pixelsize: float # this is `font_size` in Text
bold: bool
italic: bool
strikeout: bool
underline: bool
kerning: bool
halign: str
valign: str
wrap: bool
class RawObject(TypedDict):
"""The keys and their types that appear in a Tiled JSON Object.
Tiled Doc: https://doc.mapeditor.org/en/stable/reference/json-map-format/#object
"""
id: int
gid: int
template: str
x: float
y: float
width: float
height: float
rotation: float
visible: bool
name: str
type: str
properties: List[RawProperty]
ellipse: bool
point: bool
polygon: List[Dict[str, float]]
polyline: List[Dict[str, float]]
text: RawText
def _parse_common(raw_object: RawObject) -> TiledObject:
"""Create an Object containing all the attributes common to all types of objects.
Args:
raw_object: Raw object to get common attributes from
Returns:
Object: The attributes in common of all types of objects
"""
common = TiledObject(
id=raw_object["id"],
coordinates=OrderedPair(raw_object["x"], raw_object["y"]),
visible=raw_object["visible"],
size=Size(raw_object["width"], raw_object["height"]),
rotation=raw_object["rotation"],
name=raw_object["name"],
type=raw_object["type"],
)
if raw_object.get("properties") is not None:
common.properties = parse_properties(raw_object["properties"])
return common
def _parse_ellipse(raw_object: RawObject) -> Ellipse:
"""Parse the raw object into an Ellipse.
Args:
raw_object: Raw object to be parsed to an Ellipse
Returns:
Ellipse: The Ellipse object created from the raw object
"""
return Ellipse(**_parse_common(raw_object).__dict__)
def _parse_rectangle(raw_object: RawObject) -> Rectangle:
"""Parse the raw object into a Rectangle.
Args:
raw_object: Raw object to be parsed to a Rectangle
Returns:
Rectangle: The Rectangle object created from the raw object
"""
return Rectangle(**_parse_common(raw_object).__dict__)
def _parse_point(raw_object: RawObject) -> Point:
"""Parse the raw object into a Point.
Args:
raw_object: Raw object to be parsed to a Point
Returns:
Point: The Point object created from the raw object
"""
return Point(**_parse_common(raw_object).__dict__)
def _parse_polygon(raw_object: RawObject) -> Polygon:
"""Parse the raw object into a Polygon.
Args:
raw_object: Raw object to be parsed to a Polygon
Returns:
Polygon: The Polygon object created from the raw object
"""
polygon = []
for point in raw_object["polygon"]:
polygon.append(OrderedPair(point["x"], point["y"]))
return Polygon(points=polygon, **_parse_common(raw_object).__dict__)
def _parse_polyline(raw_object: RawObject) -> Polyline:
"""Parse the raw object into a Polyline.
Args:
raw_object: Raw object to be parsed to a Polyline
Returns:
Polyline: The Polyline object created from the raw object
"""
polyline = []
for point in raw_object["polyline"]:
polyline.append(OrderedPair(point["x"], point["y"]))
return Polyline(points=polyline, **_parse_common(raw_object).__dict__)
def _parse_tile(
raw_object: RawObject,
new_tileset: Optional[Dict[str, Any]] = None,
new_tileset_path: Optional[Path] = None,
) -> Tile:
"""Parse the raw object into a Tile.
Args:
raw_object: Raw object to be parsed to a Tile
Returns:
Tile: The Tile object created from the raw object
"""
gid = raw_object["gid"]
return Tile(
gid=gid,
new_tileset=new_tileset,
new_tileset_path=new_tileset_path,
**_parse_common(raw_object).__dict__
)
def _parse_text(raw_object: RawObject) -> Text:
"""Parse the raw object into Text.
Args:
raw_object: Raw object to be parsed to a Text
Returns:
Text: The Text object created from the raw object
"""
# required attributes
raw_text: RawText = raw_object["text"]
text = raw_text["text"]
# create base Text object
text_object = Text(text=text, **_parse_common(raw_object).__dict__)
# optional attributes
if raw_text.get("color") is not None:
text_object.color = parse_color(raw_text["color"])
if raw_text.get("fontfamily") is not None:
text_object.font_family = raw_text["fontfamily"]
if raw_text.get("pixelsize") is not None:
text_object.font_size = raw_text["pixelsize"]
if raw_text.get("bold") is not None:
text_object.bold = raw_text["bold"]
if raw_text.get("italic") is not None:
text_object.italic = raw_text["italic"]
if raw_text.get("kerning") is not None:
text_object.kerning = raw_text["kerning"]
if raw_text.get("strikeout") is not None:
text_object.strike_out = raw_text["strikeout"]
if raw_text.get("underline") is not None:
text_object.underline = raw_text["underline"]
if raw_text.get("halign") is not None:
text_object.horizontal_align = raw_text["halign"]
if raw_text.get("valign") is not None:
text_object.vertical_align = raw_text["valign"]
if raw_text.get("wrap") is not None:
text_object.wrap = raw_text["wrap"]
return text_object
def _get_parser(raw_object: RawObject) -> Callable[[RawObject], TiledObject]:
"""Get the parser function for a given raw object.
Only used internally by the JSON parser.
Args:
raw_object: Raw object that is analyzed to determine the parser function.
Returns:
Callable[[RawObject], Object]: The parser function.
"""
if raw_object.get("ellipse"):
return _parse_ellipse
if raw_object.get("point"):
return _parse_point
if raw_object.get("gid"):
# Only tile objects have the `gid` key
return _parse_tile
if raw_object.get("polygon"):
return _parse_polygon
if raw_object.get("polyline"):
return _parse_polyline
if raw_object.get("text"):
return _parse_text
# If it's none of the above, rectangle is the only one left.
# Rectangle is the only object which has no special properties to signify that.
return _parse_rectangle
def parse(
raw_object: RawObject,
parent_dir: Optional[Path] = None,
) -> TiledObject:
"""Parse the raw object into a pytiled_parser version
Args:
raw_object: Raw object that is to be cast.
parent_dir: The parent directory that the map file is in.
Returns:
Object: A parsed Object.
Raises:
RuntimeError: When a parameter that is conditionally required was not sent.
"""
new_tileset = None
new_tileset_path = None
if raw_object.get("template"):
if not parent_dir:
raise RuntimeError(
"A parent directory must be specified when using object templates."
)
template_path = Path(parent_dir / raw_object["template"])
template, new_tileset, new_tileset_path = load_object_template(template_path)
if isinstance(template, dict):
loaded_template = template["object"]
for key in loaded_template:
if key != "id":
raw_object[key] = loaded_template[key] # type: ignore
elif isinstance(template, etree.Element):
# load the XML object into the JSON object
raise NotImplementedError(
"Loading TMX object templates inside a JSON map is currently not supported, "
"but will be in a future release."
)
if raw_object.get("gid"):
return _parse_tile(raw_object, new_tileset, new_tileset_path)
return _get_parser(raw_object)(raw_object)

View File

@@ -0,0 +1,272 @@
from pathlib import Path
from typing import List, Optional, Union
from typing_extensions import TypedDict
from pytiled_parser.common_types import OrderedPair
from pytiled_parser.parsers.json.layer import RawLayer
from pytiled_parser.parsers.json.layer import parse as parse_layer
from pytiled_parser.parsers.json.properties import RawProperty
from pytiled_parser.parsers.json.properties import parse as parse_properties
from pytiled_parser.parsers.json.wang_set import RawWangSet
from pytiled_parser.parsers.json.wang_set import parse as parse_wangset
from pytiled_parser.tileset import Frame, Grid, Tile, Tileset, Transformations
from pytiled_parser.util import parse_color
class RawFrame(TypedDict):
"""The keys and their types that appear in a Frame JSON Object."""
duration: int
tileid: int
class RawTileOffset(TypedDict):
"""The keys and their types that appear in a TileOffset JSON Object."""
x: int
y: int
class RawTransformations(TypedDict):
"""The keys and their types that appear in a Transformations JSON Object."""
hflip: bool
vflip: bool
rotate: bool
preferuntransformed: bool
class RawTile(TypedDict):
"""The keys and their types that appear in a Tile JSON Object."""
animation: List[RawFrame]
id: int
image: str
imageheight: int
imagewidth: int
opacity: float
properties: List[RawProperty]
objectgroup: RawLayer
type: str
class RawGrid(TypedDict):
"""The keys and their types that appear in a Grid JSON Object."""
height: int
width: int
orientation: str
class RawTileSet(TypedDict):
"""The keys and their types that appear in a TileSet JSON Object."""
backgroundcolor: str
columns: int
firstgid: int
grid: RawGrid
image: str
imageheight: int
imagewidth: int
margin: int
name: str
properties: List[RawProperty]
source: str
spacing: int
tilecount: int
tiledversion: str
tileheight: int
tileoffset: RawTileOffset
tiles: List[RawTile]
tilewidth: int
transparentcolor: str
transformations: RawTransformations
version: Union[str, float]
wangsets: List[RawWangSet]
def _parse_frame(raw_frame: RawFrame) -> Frame:
"""Parse the raw_frame to a Frame.
Args:
raw_frame: RawFrame to be parsed to a Frame
Returns:
Frame: The Frame created from the raw_frame
"""
return Frame(duration=raw_frame["duration"], tile_id=raw_frame["tileid"])
def _parse_tile_offset(raw_tile_offset: RawTileOffset) -> OrderedPair:
"""Parse the raw_tile_offset to an OrderedPair.
Args:
raw_tile_offset: RawTileOffset to be parsed to an OrderedPair
Returns:
OrderedPair: The OrderedPair created from the raw_tile_offset
"""
return OrderedPair(raw_tile_offset["x"], raw_tile_offset["y"])
def _parse_transformations(raw_transformations: RawTransformations) -> Transformations:
"""Parse the raw_transformations to a Transformations object.
Args:
raw_transformations: RawTransformations to be parsed to a Transformations
Returns:
Transformations: The Transformations created from the raw_transformations
"""
return Transformations(
hflip=raw_transformations["hflip"],
vflip=raw_transformations["vflip"],
rotate=raw_transformations["rotate"],
prefer_untransformed=raw_transformations["preferuntransformed"],
)
def _parse_grid(raw_grid: RawGrid) -> Grid:
"""Parse the raw_grid to a Grid object.
Args:
raw_grid: RawGrid to be parsed to a Grid
Returns:
Grid: The Grid created from the raw_grid
"""
return Grid(
orientation=raw_grid["orientation"],
width=raw_grid["width"],
height=raw_grid["height"],
)
def _parse_tile(raw_tile: RawTile, external_path: Optional[Path] = None) -> Tile:
"""Parse the raw_tile to a Tile object.
Args:
raw_tile: RawTile to be parsed to a Tile
Returns:
Tile: The Tile created from the raw_tile
"""
id_ = raw_tile["id"]
tile = Tile(id=id_)
if raw_tile.get("animation") is not None:
tile.animation = []
for frame in raw_tile["animation"]:
tile.animation.append(_parse_frame(frame))
if raw_tile.get("objectgroup") is not None:
tile.objects = parse_layer(raw_tile["objectgroup"])
if raw_tile.get("properties") is not None:
tile.properties = parse_properties(raw_tile["properties"])
if raw_tile.get("image") is not None:
if external_path:
tile.image = Path(external_path / raw_tile["image"]).absolute().resolve()
else:
tile.image = Path(raw_tile["image"])
if raw_tile.get("imagewidth") is not None:
tile.image_width = raw_tile["imagewidth"]
if raw_tile.get("imageheight") is not None:
tile.image_height = raw_tile["imageheight"]
if raw_tile.get("type") is not None:
tile.type = raw_tile["type"]
return tile
def parse(
raw_tileset: RawTileSet,
firstgid: int,
external_path: Optional[Path] = None,
) -> Tileset:
"""Parse the raw tileset into a pytiled_parser type
Args:
raw_tileset: Raw Tileset to be parsed.
firstgid: GID corresponding the first tile in the set.
external_path: The path to the tileset if it is not an embedded one.
Returns:
TileSet: a properly typed TileSet.
"""
tileset = Tileset(
name=raw_tileset["name"],
tile_count=raw_tileset["tilecount"],
tile_width=raw_tileset["tilewidth"],
tile_height=raw_tileset["tileheight"],
columns=raw_tileset["columns"],
spacing=raw_tileset["spacing"],
margin=raw_tileset["margin"],
firstgid=firstgid,
)
if raw_tileset.get("version") is not None:
if isinstance(raw_tileset["version"], float):
tileset.version = str(raw_tileset["version"])
else:
tileset.version = raw_tileset["version"]
if raw_tileset.get("tiledversion") is not None:
tileset.tiled_version = raw_tileset["tiledversion"]
if raw_tileset.get("image") is not None:
if external_path:
tileset.image = (
Path(external_path / raw_tileset["image"]).absolute().resolve()
)
else:
tileset.image = Path(raw_tileset["image"])
if raw_tileset.get("imagewidth") is not None:
tileset.image_width = raw_tileset["imagewidth"]
if raw_tileset.get("imageheight") is not None:
tileset.image_height = raw_tileset["imageheight"]
if raw_tileset.get("backgroundcolor") is not None:
tileset.background_color = parse_color(raw_tileset["backgroundcolor"])
if raw_tileset.get("tileoffset") is not None:
tileset.tile_offset = _parse_tile_offset(raw_tileset["tileoffset"])
if raw_tileset.get("transparentcolor") is not None:
tileset.transparent_color = parse_color(raw_tileset["transparentcolor"])
if raw_tileset.get("grid") is not None:
tileset.grid = _parse_grid(raw_tileset["grid"])
if raw_tileset.get("properties") is not None:
tileset.properties = parse_properties(raw_tileset["properties"])
if raw_tileset.get("tiles") is not None:
tiles = {}
for raw_tile in raw_tileset["tiles"]:
tiles[raw_tile["id"]] = _parse_tile(raw_tile, external_path=external_path)
tileset.tiles = tiles
if raw_tileset.get("wangsets") is not None:
wangsets = []
for raw_wangset in raw_tileset["wangsets"]:
wangsets.append(parse_wangset(raw_wangset))
tileset.wang_sets = wangsets
if raw_tileset.get("transformations") is not None:
tileset.transformations = _parse_transformations(raw_tileset["transformations"])
return tileset

View File

@@ -0,0 +1,104 @@
from typing import List
from typing_extensions import TypedDict
from pytiled_parser.parsers.json.properties import RawProperty
from pytiled_parser.parsers.json.properties import parse as parse_properties
from pytiled_parser.util import parse_color
from pytiled_parser.wang_set import WangColor, WangSet, WangTile
class RawWangTile(TypedDict):
"""The keys and their types that appear in a Wang Tile JSON Object."""
tileid: int
# Tiled stores these IDs as a list represented like so:
# [top, top_right, right, bottom_right, bottom, bottom_left, left, top_left]
wangid: List[int]
class RawWangColor(TypedDict):
"""The keys and their types that appear in a Wang Color JSON Object."""
color: str
name: str
probability: float
tile: int
properties: List[RawProperty]
class RawWangSet(TypedDict):
"""The keys and their types that appear in a Wang Set JSON Object."""
colors: List[RawWangColor]
name: str
properties: List[RawProperty]
tile: int
type: str
wangtiles: List[RawWangTile]
def _parse_wang_tile(raw_wang_tile: RawWangTile) -> WangTile:
"""Parse the raw wang tile into a pytiled_parser type
Args:
raw_wang_tile: RawWangTile to be parsed.
Returns:
WangTile: A properly typed WangTile.
"""
return WangTile(tile_id=raw_wang_tile["tileid"], wang_id=raw_wang_tile["wangid"])
def _parse_wang_color(raw_wang_color: RawWangColor) -> WangColor:
"""Parse the raw wang color into a pytiled_parser type
Args:
raw_wang_color: RawWangColor to be parsed.
Returns:
WangColor: A properly typed WangColor.
"""
wang_color = WangColor(
name=raw_wang_color["name"],
color=parse_color(raw_wang_color["color"]),
tile=raw_wang_color["tile"],
probability=raw_wang_color["probability"],
)
if raw_wang_color.get("properties") is not None:
wang_color.properties = parse_properties(raw_wang_color["properties"])
return wang_color
def parse(raw_wangset: RawWangSet) -> WangSet:
"""Parse the raw wangset into a pytiled_parser type
Args:
raw_wangset: Raw Wangset to be parsed.
Returns:
WangSet: A properly typed WangSet.
"""
colors = []
for raw_wang_color in raw_wangset["colors"]:
colors.append(_parse_wang_color(raw_wang_color))
tiles = {}
for raw_wang_tile in raw_wangset["wangtiles"]:
tiles[raw_wang_tile["tileid"]] = _parse_wang_tile(raw_wang_tile)
wangset = WangSet(
name=raw_wangset["name"],
tile=raw_wangset["tile"],
wang_type=raw_wangset["type"],
wang_colors=colors,
wang_tiles=tiles,
)
if raw_wangset.get("properties") is not None:
wangset.properties = parse_properties(raw_wangset["properties"])
return wangset

View File

@@ -0,0 +1,360 @@
"""Layer parsing for the TMX Map Format.
"""
import base64
import gzip
import importlib.util
import xml.etree.ElementTree as etree
import zlib
from pathlib import Path
from typing import List, Optional
from pytiled_parser.common_types import OrderedPair, Size
from pytiled_parser.layer import (
Chunk,
ImageLayer,
Layer,
LayerGroup,
ObjectLayer,
TileLayer,
)
from pytiled_parser.parsers.tmx.properties import parse as parse_properties
from pytiled_parser.parsers.tmx.tiled_object import parse as parse_object
from pytiled_parser.util import parse_color
zstd_spec = importlib.util.find_spec("zstd")
if zstd_spec:
import zstd
else:
zstd = None
def _convert_raw_tile_layer_data(data: List[int], layer_width: int) -> List[List[int]]:
"""Convert raw layer data into a nested lit based on the layer width
Args:
data: The data to convert
layer_width: Width of the layer
Returns:
List[List[int]]: A nested list containing the converted data
"""
tile_grid: List[List[int]] = [[]]
column_count = 0
row_count = 0
for item in data:
column_count += 1
tile_grid[row_count].append(item)
if not column_count % layer_width and column_count < len(data):
row_count += 1
tile_grid.append([])
return tile_grid
def _decode_tile_layer_data(
data: str, compression: str, layer_width: int
) -> List[List[int]]:
"""Decode Base64 Encoded tile data. Optionally supports gzip and zlib compression.
Args:
data: The base64 encoded data
compression: Either zlib, gzip, or empty. If empty no decompression is done.
Returns:
List[List[int]]: A nested list containing the decoded data
Raises:
ValueError: For an unsupported compression type.
"""
unencoded_data = base64.b64decode(data)
if compression == "zlib":
unzipped_data = zlib.decompress(unencoded_data)
elif compression == "gzip":
unzipped_data = gzip.decompress(unencoded_data)
elif compression == "zstd" and zstd is None:
raise ValueError(
"zstd compression support is not installed."
"To install use 'pip install pytiled-parser[zstd]'"
)
elif compression == "zstd":
unzipped_data = zstd.decompress(unencoded_data)
else:
unzipped_data = unencoded_data
tile_grid: List[int] = []
byte_count = 0
int_count = 0
int_value = 0
for byte in unzipped_data:
int_value += byte << (byte_count * 8)
byte_count += 1
if not byte_count % 4:
byte_count = 0
int_count += 1
tile_grid.append(int_value)
int_value = 0
return _convert_raw_tile_layer_data(tile_grid, layer_width)
def _parse_chunk(
raw_chunk: etree.Element,
encoding: Optional[str] = None,
compression: Optional[str] = None,
) -> Chunk:
"""Parse the raw_chunk to a Chunk.
Args:
raw_chunk: XML Element to be parsed to a Chunk
encoding: Encoding type. ("base64" or None)
compression: Either zlib, gzip, or empty. If empty no decompression is done.
Returns:
Chunk: The Chunk created from the raw_chunk
"""
if encoding == "base64":
assert isinstance(compression, str)
data = _decode_tile_layer_data(
raw_chunk.text, compression, int(raw_chunk.attrib["width"]) # type: ignore
)
else:
data = _convert_raw_tile_layer_data(
[int(v.strip()) for v in raw_chunk.text.split(",")], # type: ignore
int(raw_chunk.attrib["width"]),
)
return Chunk(
coordinates=OrderedPair(int(raw_chunk.attrib["x"]), int(raw_chunk.attrib["y"])),
size=Size(int(raw_chunk.attrib["width"]), int(raw_chunk.attrib["height"])),
data=data,
)
def _parse_common(raw_layer: etree.Element) -> Layer:
"""Create a Layer containing all the attributes common to all layer types.
This is to create the stub Layer object that can then be used to create the actual
specific sub-classes of Layer.
Args:
raw_layer: XML Element to get common attributes from
Returns:
Layer: The attributes in common of all layer types
"""
if raw_layer.attrib.get("name") is None:
raw_layer.attrib["name"] = ""
common = Layer(
name=raw_layer.attrib["name"],
)
if raw_layer.attrib.get("opacity") is not None:
common.opacity = float(raw_layer.attrib["opacity"])
if raw_layer.attrib.get("visible") is not None:
common.visible = bool(int(raw_layer.attrib["visible"]))
if raw_layer.attrib.get("id") is not None:
common.id = int(raw_layer.attrib["id"])
if raw_layer.attrib.get("offsetx") is not None:
common.offset = OrderedPair(
float(raw_layer.attrib["offsetx"]), float(raw_layer.attrib["offsety"])
)
properties_element = raw_layer.find("./properties")
if properties_element is not None:
common.properties = parse_properties(properties_element)
parallax = [1.0, 1.0]
if raw_layer.attrib.get("parallaxx") is not None:
parallax[0] = float(raw_layer.attrib["parallaxx"])
if raw_layer.attrib.get("parallaxy") is not None:
parallax[1] = float(raw_layer.attrib["parallaxy"])
common.parallax_factor = OrderedPair(parallax[0], parallax[1])
if raw_layer.attrib.get("tintcolor") is not None:
common.tint_color = parse_color(raw_layer.attrib["tintcolor"])
return common
def _parse_tile_layer(raw_layer: etree.Element) -> TileLayer:
"""Parse the raw_layer to a TileLayer.
Args:
raw_layer: XML Element to be parsed to a TileLayer.
Returns:
TileLayer: The TileLayer created from raw_layer
"""
common = _parse_common(raw_layer).__dict__
del common["size"]
tile_layer = TileLayer(
size=Size(int(raw_layer.attrib["width"]), int(raw_layer.attrib["height"])),
**common,
)
data_element = raw_layer.find("data")
if data_element is not None:
encoding = None
if data_element.attrib.get("encoding") is not None:
encoding = data_element.attrib["encoding"]
compression = ""
if data_element.attrib.get("compression") is not None:
compression = data_element.attrib["compression"]
raw_chunks = data_element.findall("chunk")
if not raw_chunks:
if encoding and encoding != "csv":
tile_layer.data = _decode_tile_layer_data(
data=data_element.text, # type: ignore
compression=compression,
layer_width=int(raw_layer.attrib["width"]),
)
else:
tile_layer.data = _convert_raw_tile_layer_data(
[int(v.strip()) for v in data_element.text.split(",")], # type: ignore
int(raw_layer.attrib["width"]),
)
else:
chunks = []
for raw_chunk in raw_chunks:
chunks.append(
_parse_chunk(
raw_chunk,
encoding,
compression,
)
)
if chunks:
tile_layer.chunks = chunks
return tile_layer
def _parse_object_layer(
raw_layer: etree.Element, parent_dir: Optional[Path] = None
) -> ObjectLayer:
"""Parse the raw_layer to an ObjectLayer.
Args:
raw_layer: XML Element to be parsed to an ObjectLayer.
Returns:
ObjectLayer: The ObjectLayer created from raw_layer
"""
objects = []
for object_ in raw_layer.findall("./object"):
objects.append(parse_object(object_, parent_dir))
object_layer = ObjectLayer(
tiled_objects=objects,
**_parse_common(raw_layer).__dict__,
)
if raw_layer.attrib.get("draworder") is not None:
object_layer.draw_order = raw_layer.attrib["draworder"]
return object_layer
def _parse_image_layer(raw_layer: etree.Element) -> ImageLayer:
"""Parse the raw_layer to an ImageLayer.
Args:
raw_layer: XML Element to be parsed to an ImageLayer.
Returns:
ImageLayer: The ImageLayer created from raw_layer
"""
image_element = raw_layer.find("./image")
if image_element is not None:
source = Path(image_element.attrib["source"])
transparent_color = None
if image_element.attrib.get("trans") is not None:
transparent_color = parse_color(image_element.attrib["trans"])
image_layer = ImageLayer(
image=source,
transparent_color=transparent_color,
**_parse_common(raw_layer).__dict__,
)
print(image_layer.size)
return image_layer
raise RuntimeError("Tried to parse an image layer that doesn't have an image!")
def _parse_group_layer(
raw_layer: etree.Element, parent_dir: Optional[Path] = None
) -> LayerGroup:
"""Parse the raw_layer to a LayerGroup.
Args:
raw_layer: XML Element to be parsed to a LayerGroup.
Returns:
LayerGroup: The LayerGroup created from raw_layer
"""
layers: List[Layer] = []
for layer in raw_layer.findall("./layer"):
layers.append(_parse_tile_layer(layer))
for layer in raw_layer.findall("./objectgroup"):
layers.append(_parse_object_layer(layer, parent_dir))
for layer in raw_layer.findall("./imagelayer"):
layers.append(_parse_image_layer(layer))
for layer in raw_layer.findall("./group"):
layers.append(_parse_group_layer(layer, parent_dir))
# layers = []
# layers = [
# parse(child_layer, parent_dir=parent_dir)
# for child_layer in raw_layer.iter()
# if child_layer.tag in ["layer", "objectgroup", "imagelayer", "group"]
# ]
return LayerGroup(layers=layers, **_parse_common(raw_layer).__dict__)
def parse(
raw_layer: etree.Element,
parent_dir: Optional[Path] = None,
) -> Layer:
"""Parse a raw Layer into a pytiled_parser object.
This function will determine the type of layer and parse accordingly.
Args:
raw_layer: Raw layer to be parsed.
parent_dir: The parent directory that the map file is in.
Returns:
Layer: A parsed Layer.
Raises:
RuntimeError: For an invalid layer type being provided
"""
type_ = raw_layer.tag
if type_ == "objectgroup":
return _parse_object_layer(raw_layer, parent_dir)
elif type_ == "group":
return _parse_group_layer(raw_layer, parent_dir)
elif type_ == "imagelayer":
return _parse_image_layer(raw_layer)
elif type_ == "layer":
return _parse_tile_layer(raw_layer)
raise RuntimeError(f"An invalid layer type of {type_} was supplied")

View File

@@ -0,0 +1,33 @@
import xml.etree.ElementTree as etree
from pathlib import Path
from typing import List, Union, cast
from pytiled_parser.properties import Properties, Property
from pytiled_parser.util import parse_color
def parse(raw_properties: etree.Element) -> Properties:
final: Properties = {}
value: Property
for raw_property in raw_properties.findall("property"):
type_ = raw_property.attrib.get("type")
value_ = raw_property.attrib["value"]
if type_ == "file":
value = Path(value_)
elif type_ == "color":
value = parse_color(value_)
elif type_ == "int" or type_ == "float":
value = float(value_)
elif type_ == "bool":
if value_ == "true":
value = True
else:
value = False
else:
value = value_
final[raw_property.attrib["name"]] = value
return final

View File

@@ -0,0 +1,132 @@
import json
import xml.etree.ElementTree as etree
from pathlib import Path
from pytiled_parser.common_types import OrderedPair, Size
from pytiled_parser.exception import UnknownFormat
from pytiled_parser.parsers.json.tileset import parse as parse_json_tileset
from pytiled_parser.parsers.tmx.layer import parse as parse_layer
from pytiled_parser.parsers.tmx.properties import parse as parse_properties
from pytiled_parser.parsers.tmx.tileset import parse as parse_tmx_tileset
from pytiled_parser.tiled_map import TiledMap, TilesetDict
from pytiled_parser.util import check_format, parse_color
def parse(file: Path) -> TiledMap:
"""Parse the raw Tiled map into a pytiled_parser type.
Args:
file: Path to the map file.
Returns:
TiledMap: A parsed TiledMap.
"""
with open(file) as map_file:
raw_map = etree.parse(map_file).getroot()
parent_dir = file.parent
raw_tilesets = raw_map.findall("./tileset")
tilesets: TilesetDict = {}
for raw_tileset in raw_tilesets:
if raw_tileset.attrib.get("source") is not None:
# Is an external Tileset
tileset_path = Path(parent_dir / raw_tileset.attrib["source"])
parser = check_format(tileset_path)
with open(tileset_path) as tileset_file:
if parser == "tmx":
raw_tileset_external = etree.parse(tileset_file).getroot()
tilesets[int(raw_tileset.attrib["firstgid"])] = parse_tmx_tileset(
raw_tileset_external,
int(raw_tileset.attrib["firstgid"]),
external_path=tileset_path.parent,
)
elif parser == "json":
tilesets[int(raw_tileset.attrib["firstgid"])] = parse_json_tileset(
json.load(tileset_file),
int(raw_tileset.attrib["firstgid"]),
external_path=tileset_path.parent,
)
else:
raise UnknownFormat(
"Unkown Tileset format, please use either the TSX or JSON format."
)
else:
# Is an embedded Tileset
tilesets[int(raw_tileset.attrib["firstgid"])] = parse_tmx_tileset(
raw_tileset, int(raw_tileset.attrib["firstgid"])
)
layers = []
for element in raw_map.iter():
if element.tag in ["layer", "objectgroup", "imagelayer", "group"]:
layers.append(parse_layer(element, parent_dir))
map_ = TiledMap(
map_file=file,
infinite=bool(int(raw_map.attrib["infinite"])),
layers=layers,
map_size=Size(int(raw_map.attrib["width"]), int(raw_map.attrib["height"])),
next_layer_id=int(raw_map.attrib["nextlayerid"]),
next_object_id=int(raw_map.attrib["nextobjectid"]),
orientation=raw_map.attrib["orientation"],
render_order=raw_map.attrib["renderorder"],
tiled_version=raw_map.attrib["tiledversion"],
tile_size=Size(
int(raw_map.attrib["tilewidth"]), int(raw_map.attrib["tileheight"])
),
tilesets=tilesets,
version=raw_map.attrib["version"],
)
layers = [layer for layer in map_.layers if hasattr(layer, "tiled_objects")]
for my_layer in layers:
for tiled_object in my_layer.tiled_objects:
if hasattr(tiled_object, "new_tileset"):
if tiled_object.new_tileset is not None:
already_loaded = None
for val in map_.tilesets.values():
if val.name == tiled_object.new_tileset.attrib["name"]:
already_loaded = val
break
if not already_loaded:
print("here")
highest_firstgid = max(map_.tilesets.keys())
last_tileset_count = map_.tilesets[highest_firstgid].tile_count
new_firstgid = highest_firstgid + last_tileset_count
map_.tilesets[new_firstgid] = parse_tmx_tileset(
tiled_object.new_tileset,
new_firstgid,
tiled_object.new_tileset_path,
)
tiled_object.gid = tiled_object.gid + (new_firstgid - 1)
else:
tiled_object.gid = tiled_object.gid + (
already_loaded.firstgid - 1
)
tiled_object.new_tileset = None
tiled_object.new_tileset_path = None
if raw_map.attrib.get("backgroundcolor") is not None:
map_.background_color = parse_color(raw_map.attrib["backgroundcolor"])
if raw_map.attrib.get("hexsidelength") is not None:
map_.hex_side_length = int(raw_map.attrib["hexsidelength"])
properties_element = raw_map.find("./properties")
if properties_element:
map_.properties = parse_properties(properties_element)
if raw_map.attrib.get("staggeraxis") is not None:
map_.stagger_axis = raw_map.attrib["staggeraxis"]
if raw_map.attrib.get("staggerindex") is not None:
map_.stagger_index = raw_map.attrib["staggerindex"]
return map_

View File

@@ -0,0 +1,293 @@
import json
import xml.etree.ElementTree as etree
from pathlib import Path
from typing import Callable, Optional
from pytiled_parser.common_types import OrderedPair, Size
from pytiled_parser.parsers.tmx.properties import parse as parse_properties
from pytiled_parser.tiled_object import (
Ellipse,
Point,
Polygon,
Polyline,
Rectangle,
Text,
Tile,
TiledObject,
)
from pytiled_parser.util import load_object_template, parse_color
def _parse_common(raw_object: etree.Element) -> TiledObject:
"""Create an Object containing all the attributes common to all types of objects.
Args:
raw_object: XML Element to get common attributes from
Returns:
Object: The attributes in common of all types of objects
"""
common = TiledObject(
id=int(raw_object.attrib["id"]),
coordinates=OrderedPair(
float(raw_object.attrib["x"]), float(raw_object.attrib["y"])
),
)
if raw_object.attrib.get("width") is not None:
common.size = Size(
float(raw_object.attrib["width"]), float(raw_object.attrib["height"])
)
if raw_object.attrib.get("visible") is not None:
common.visible = bool(int(raw_object.attrib["visible"]))
if raw_object.attrib.get("rotation") is not None:
common.rotation = float(raw_object.attrib["rotation"])
if raw_object.attrib.get("name") is not None:
common.name = raw_object.attrib["name"]
if raw_object.attrib.get("type") is not None:
common.type = raw_object.attrib["type"]
properties_element = raw_object.find("./properties")
if properties_element:
common.properties = parse_properties(properties_element)
return common
def _parse_ellipse(raw_object: etree.Element) -> Ellipse:
"""Parse the raw object into an Ellipse.
Args:
raw_object: XML Element to be parsed to an Ellipse
Returns:
Ellipse: The Ellipse object created from the raw object
"""
return Ellipse(**_parse_common(raw_object).__dict__)
def _parse_rectangle(raw_object: etree.Element) -> Rectangle:
"""Parse the raw object into a Rectangle.
Args:
raw_object: XML Element to be parsed to a Rectangle
Returns:
Rectangle: The Rectangle object created from the raw object
"""
return Rectangle(**_parse_common(raw_object).__dict__)
def _parse_point(raw_object: etree.Element) -> Point:
"""Parse the raw object into a Point.
Args:
raw_object: XML Element to be parsed to a Point
Returns:
Point: The Point object created from the raw object
"""
return Point(**_parse_common(raw_object).__dict__)
def _parse_polygon(raw_object: etree.Element) -> Polygon:
"""Parse the raw object into a Polygon.
Args:
raw_object: XML Element to be parsed to a Polygon
Returns:
Polygon: The Polygon object created from the raw object
"""
polygon = []
polygon_element = raw_object.find("./polygon")
if polygon_element is not None:
for raw_point in polygon_element.attrib["points"].split(" "):
point = raw_point.split(",")
polygon.append(OrderedPair(float(point[0]), float(point[1])))
return Polygon(points=polygon, **_parse_common(raw_object).__dict__)
def _parse_polyline(raw_object: etree.Element) -> Polyline:
"""Parse the raw object into a Polyline.
Args:
raw_object: Raw object to be parsed to a Polyline
Returns:
Polyline: The Polyline object created from the raw object
"""
polyline = []
polyline_element = raw_object.find("./polyline")
if polyline_element is not None:
for raw_point in polyline_element.attrib["points"].split(" "):
point = raw_point.split(",")
polyline.append(OrderedPair(float(point[0]), float(point[1])))
return Polyline(points=polyline, **_parse_common(raw_object).__dict__)
def _parse_tile(
raw_object: etree.Element,
new_tileset: Optional[etree.Element] = None,
new_tileset_path: Optional[Path] = None,
) -> Tile:
"""Parse the raw object into a Tile.
Args:
raw_object: XML Element to be parsed to a Tile
Returns:
Tile: The Tile object created from the raw object
"""
return Tile(
gid=int(raw_object.attrib["gid"]),
new_tileset=new_tileset,
new_tileset_path=new_tileset_path,
**_parse_common(raw_object).__dict__
)
def _parse_text(raw_object: etree.Element) -> Text:
"""Parse the raw object into Text.
Args:
raw_object: XML Element to be parsed to a Text
Returns:
Text: The Text object created from the raw object
"""
# required attributes
text_element = raw_object.find("./text")
if text_element is not None:
text = text_element.text
if not text:
text = ""
# create base Text object
text_object = Text(text=text, **_parse_common(raw_object).__dict__)
# optional attributes
if text_element.attrib.get("color") is not None:
text_object.color = parse_color(text_element.attrib["color"])
if text_element.attrib.get("fontfamily") is not None:
text_object.font_family = text_element.attrib["fontfamily"]
if text_element.attrib.get("pixelsize") is not None:
text_object.font_size = float(text_element.attrib["pixelsize"])
if text_element.attrib.get("bold") is not None:
text_object.bold = bool(int(text_element.attrib["bold"]))
if text_element.attrib.get("italic") is not None:
text_object.italic = bool(int(text_element.attrib["italic"]))
if text_element.attrib.get("kerning") is not None:
text_object.kerning = bool(int(text_element.attrib["kerning"]))
if text_element.attrib.get("strikeout") is not None:
text_object.strike_out = bool(int(text_element.attrib["strikeout"]))
if text_element.attrib.get("underline") is not None:
text_object.underline = bool(int(text_element.attrib["underline"]))
if text_element.attrib.get("halign") is not None:
text_object.horizontal_align = text_element.attrib["halign"]
if text_element.attrib.get("valign") is not None:
text_object.vertical_align = text_element.attrib["valign"]
if text_element.attrib.get("wrap") is not None:
text_object.wrap = bool(int(text_element.attrib["wrap"]))
return text_object
def _get_parser(raw_object: etree.Element) -> Callable[[etree.Element], TiledObject]:
"""Get the parser function for a given raw object.
Only used internally by the TMX parser.
Args:
raw_object: XML Element that is analyzed to determine the parser function.
Returns:
Callable[[Element], Object]: The parser function.
"""
if raw_object.find("./ellipse") is not None:
return _parse_ellipse
if raw_object.find("./point") is not None:
return _parse_point
if raw_object.find("./polygon") is not None:
return _parse_polygon
if raw_object.find("./polyline") is not None:
return _parse_polyline
if raw_object.find("./text") is not None:
return _parse_text
# If it's none of the above, rectangle is the only one left.
# Rectangle is the only object which has no properties to signify that.
return _parse_rectangle
def parse(raw_object: etree.Element, parent_dir: Optional[Path] = None) -> TiledObject:
"""Parse the raw object into a pytiled_parser version
Args:
raw_object: XML Element that is to be parsed.
parent_dir: The parent directory that the map file is in.
Returns:
TiledObject: A parsed Object.
Raises:
RuntimeError: When a parameter that is conditionally required was not sent.
"""
new_tileset = None
new_tileset_path = None
if raw_object.attrib.get("template"):
if not parent_dir:
raise RuntimeError(
"A parent directory must be specified when using object templates."
)
template_path = Path(parent_dir / raw_object.attrib["template"])
template, new_tileset, new_tileset_path = load_object_template(template_path)
if isinstance(template, etree.Element):
new_object = template.find("./object")
if new_object is not None:
if raw_object.attrib.get("id") is not None:
new_object.attrib["id"] = raw_object.attrib["id"]
if raw_object.attrib.get("x") is not None:
new_object.attrib["x"] = raw_object.attrib["x"]
if raw_object.attrib.get("y") is not None:
new_object.attrib["y"] = raw_object.attrib["y"]
raw_object = new_object
elif isinstance(template, dict):
# load the JSON object into the XML object
raise NotImplementedError(
"Loading JSON object templates inside a TMX map is currently not supported, "
"but will be in a future release."
)
if raw_object.attrib.get("gid"):
return _parse_tile(raw_object, new_tileset, new_tileset_path)
return _get_parser(raw_object)(raw_object)

View File

@@ -0,0 +1,194 @@
import xml.etree.ElementTree as etree
from pathlib import Path
from typing import Optional
from pytiled_parser.common_types import OrderedPair
from pytiled_parser.parsers.tmx.layer import parse as parse_layer
from pytiled_parser.parsers.tmx.properties import parse as parse_properties
from pytiled_parser.parsers.tmx.wang_set import parse as parse_wangset
from pytiled_parser.tileset import Frame, Grid, Tile, Tileset, Transformations
from pytiled_parser.util import parse_color
def _parse_frame(raw_frame: etree.Element) -> Frame:
"""Parse the raw_frame to a Frame object.
Args:
raw_frame: XML Element to be parsed to a Frame
Returns:
Frame: The Frame created from the raw_frame
"""
return Frame(
duration=int(raw_frame.attrib["duration"]),
tile_id=int(raw_frame.attrib["tileid"]),
)
def _parse_grid(raw_grid: etree.Element) -> Grid:
"""Parse the raw_grid to a Grid object.
Args:
raw_grid: XML Element to be parsed to a Grid
Returns:
Grid: The Grid created from the raw_grid
"""
return Grid(
orientation=raw_grid.attrib["orientation"],
width=int(raw_grid.attrib["width"]),
height=int(raw_grid.attrib["height"]),
)
def _parse_transformations(raw_transformations: etree.Element) -> Transformations:
"""Parse the raw_transformations to a Transformations object.
Args:
raw_transformations: XML Element to be parsed to a Transformations
Returns:
Transformations: The Transformations created from the raw_transformations
"""
return Transformations(
hflip=bool(int(raw_transformations.attrib["hflip"])),
vflip=bool(int(raw_transformations.attrib["vflip"])),
rotate=bool(int(raw_transformations.attrib["rotate"])),
prefer_untransformed=bool(
int(raw_transformations.attrib["preferuntransformed"])
),
)
def _parse_tile(raw_tile: etree.Element, external_path: Optional[Path] = None) -> Tile:
"""Parse the raw_tile to a Tile object.
Args:
raw_tile: XML Element to be parsed to a Tile
Returns:
Tile: The Tile created from the raw_tile
"""
tile = Tile(id=int(raw_tile.attrib["id"]))
if raw_tile.attrib.get("type") is not None:
tile.type = raw_tile.attrib["type"]
animation_element = raw_tile.find("./animation")
if animation_element is not None:
tile.animation = []
for raw_frame in animation_element.findall("./frame"):
tile.animation.append(_parse_frame(raw_frame))
object_element = raw_tile.find("./objectgroup")
if object_element is not None:
tile.objects = parse_layer(object_element)
properties_element = raw_tile.find("./properties")
if properties_element is not None:
tile.properties = parse_properties(properties_element)
image_element = raw_tile.find("./image")
if image_element is not None:
if external_path:
tile.image = (
Path(external_path / image_element.attrib["source"])
.absolute()
.resolve()
)
else:
tile.image = Path(image_element.attrib["source"])
tile.image_width = int(image_element.attrib["width"])
tile.image_height = int(image_element.attrib["height"])
return tile
def parse(
raw_tileset: etree.Element,
firstgid: int,
external_path: Optional[Path] = None,
) -> Tileset:
tileset = Tileset(
name=raw_tileset.attrib["name"],
tile_count=int(raw_tileset.attrib["tilecount"]),
tile_width=int(raw_tileset.attrib["tilewidth"]),
tile_height=int(raw_tileset.attrib["tileheight"]),
columns=int(raw_tileset.attrib["columns"]),
firstgid=firstgid,
)
if raw_tileset.attrib.get("version") is not None:
tileset.version = raw_tileset.attrib["version"]
if raw_tileset.attrib.get("tiledversion") is not None:
tileset.tiled_version = raw_tileset.attrib["tiledversion"]
if raw_tileset.attrib.get("backgroundcolor") is not None:
tileset.background_color = parse_color(raw_tileset.attrib["backgroundcolor"])
if raw_tileset.attrib.get("spacing") is not None:
tileset.spacing = int(raw_tileset.attrib["spacing"])
if raw_tileset.attrib.get("margin") is not None:
tileset.margin = int(raw_tileset.attrib["margin"])
image_element = raw_tileset.find("image")
if image_element is not None:
if external_path:
tileset.image = (
Path(external_path / image_element.attrib["source"])
.absolute()
.resolve()
)
else:
tileset.image = Path(image_element.attrib["source"])
tileset.image_width = int(image_element.attrib["width"])
tileset.image_height = int(image_element.attrib["height"])
if image_element.attrib.get("trans") is not None:
my_string = image_element.attrib["trans"]
if my_string[0] != "#":
my_string = f"#{my_string}"
tileset.transparent_color = parse_color(my_string)
tileoffset_element = raw_tileset.find("./tileoffset")
if tileoffset_element is not None:
tileset.tile_offset = OrderedPair(
int(tileoffset_element.attrib["x"]), int(tileoffset_element.attrib["y"])
)
grid_element = raw_tileset.find("./grid")
if grid_element is not None:
tileset.grid = _parse_grid(grid_element)
properties_element = raw_tileset.find("./properties")
if properties_element is not None:
tileset.properties = parse_properties(properties_element)
tiles = {}
for tile_element in raw_tileset.findall("./tile"):
tiles[int(tile_element.attrib["id"])] = _parse_tile(
tile_element, external_path=external_path
)
if tiles:
tileset.tiles = tiles
wangsets_element = raw_tileset.find("./wangsets")
if wangsets_element is not None:
wangsets = []
for raw_wangset in wangsets_element.findall("./wangset"):
wangsets.append(parse_wangset(raw_wangset))
tileset.wang_sets = wangsets
transformations_element = raw_tileset.find("./transformations")
if transformations_element is not None:
tileset.transformations = _parse_transformations(transformations_element)
return tileset

View File

@@ -0,0 +1,74 @@
import xml.etree.ElementTree as etree
from pytiled_parser.parsers.tmx.properties import parse as parse_properties
from pytiled_parser.util import parse_color
from pytiled_parser.wang_set import WangColor, WangSet, WangTile
def _parse_wang_tile(raw_wang_tile: etree.Element) -> WangTile:
"""Parse the raw wang tile into a pytiled_parser type
Args:
raw_wang_tile: XML Element to be parsed.
Returns:
WangTile: A properly typed WangTile.
"""
ids = [int(v.strip()) for v in raw_wang_tile.attrib["wangid"].split(",")]
return WangTile(tile_id=int(raw_wang_tile.attrib["tileid"]), wang_id=ids)
def _parse_wang_color(raw_wang_color: etree.Element) -> WangColor:
"""Parse the raw wang color into a pytiled_parser type
Args:
raw_wang_color: XML Element to be parsed.
Returns:
WangColor: A properly typed WangColor.
"""
wang_color = WangColor(
name=raw_wang_color.attrib["name"],
color=parse_color(raw_wang_color.attrib["color"]),
tile=int(raw_wang_color.attrib["tile"]),
probability=float(raw_wang_color.attrib["probability"]),
)
properties = raw_wang_color.find("./properties")
if properties:
wang_color.properties = parse_properties(properties)
return wang_color
def parse(raw_wangset: etree.Element) -> WangSet:
"""Parse the raw wangset into a pytiled_parser type
Args:
raw_wangset: XML Element to be parsed.
Returns:
WangSet: A properly typed WangSet.
"""
colors = []
for raw_wang_color in raw_wangset.findall("./wangcolor"):
colors.append(_parse_wang_color(raw_wang_color))
tiles = {}
for raw_wang_tile in raw_wangset.findall("./wangtile"):
tiles[int(raw_wang_tile.attrib["tileid"])] = _parse_wang_tile(raw_wang_tile)
wangset = WangSet(
name=raw_wangset.attrib["name"],
tile=int(raw_wangset.attrib["tile"]),
wang_type=raw_wangset.attrib["type"],
wang_colors=colors,
wang_tiles=tiles,
)
properties = raw_wangset.find("./properties")
if properties:
wangset.properties = parse_properties(properties)
return wangset

View File

@@ -1,55 +1,18 @@
"""Properties Module
This module casts raw properties from Tiled maps into a dictionary of
properly typed Properties.
This module defines types for Property objects.
For more about properties in Tiled maps see the below link:
https://doc.mapeditor.org/en/stable/manual/custom-properties/
The types defined in this module get added to other objects
such as Layers, Maps, Objects, etc
"""
from pathlib import Path
from typing import Dict, List, Union
from typing import cast as type_cast
from typing_extensions import TypedDict
from typing import Dict, Union
from .common_types import Color
from .util import parse_color
Property = Union[float, Path, str, bool, Color]
Properties = Dict[str, Property]
RawValue = Union[float, str, bool]
class RawProperty(TypedDict):
"""A dictionary of raw properties."""
name: str
type: str
value: RawValue
def cast(raw_properties: List[RawProperty]) -> Properties:
"""Cast a list of `RawProperty`s into `Properties`
Args:
raw_properties: The list of `RawProperty`s to cast.
Returns:
Properties: The casted `Properties`.
"""
final: Properties = {}
value: Property
for property_ in raw_properties:
if property_["type"] == "file":
value = Path(type_cast(str, property_["value"]))
elif property_["type"] == "color":
value = parse_color(type_cast(str, property_["value"]))
else:
value = property_["value"]
final[property_["name"]] = value
return final

View File

@@ -1,19 +1,12 @@
# pylint: disable=too-few-public-methods
import json
from pathlib import Path
from typing import Dict, List, Optional, Union
from typing import cast as typing_cast
from typing import Dict, List, Optional
import attr
from typing_extensions import TypedDict
from . import layer, properties, tileset
from .common_types import Color, Size
from .layer import Layer, RawLayer
from .properties import Properties, RawProperty
from .tileset import RawTileSet, Tileset
from .util import parse_color
from pytiled_parser.common_types import Color, Size
from pytiled_parser.layer import Layer
from pytiled_parser.properties import Properties
from pytiled_parser.tileset import Tileset
TilesetDict = Dict[int, Tileset]
@@ -68,146 +61,3 @@ class TiledMap:
hex_side_length: Optional[int] = None
stagger_axis: Optional[str] = None
stagger_index: Optional[str] = None
class _RawTilesetMapping(TypedDict):
""" The way that tilesets are stored in the Tiled JSON formatted map."""
firstgid: int
source: str
class _RawTiledMap(TypedDict):
"""The keys and their types that appear in a Tiled JSON Map.
Keys:
compressionlevel: not documented - https://github.com/bjorn/tiled/issues/2815
"""
backgroundcolor: str
compressionlevel: int
height: int
hexsidelength: int
infinite: bool
layers: List[RawLayer]
nextlayerid: int
nextobjectid: int
orientation: str
properties: List[RawProperty]
renderorder: str
staggeraxis: str
staggerindex: str
tiledversion: str
tileheight: int
tilesets: List[_RawTilesetMapping]
tilewidth: int
type: str
version: Union[str, float]
width: int
def parse_map(file: Path) -> TiledMap:
"""Parse the raw Tiled map into a pytiled_parser type
Args:
file: Path to the map's JSON file
Returns:
TileSet: a properly typed TileSet.
"""
with open(file) as map_file:
raw_tiled_map = json.load(map_file)
parent_dir = file.parent
raw_tilesets: List[Union[RawTileSet, _RawTilesetMapping]] = raw_tiled_map[
"tilesets"
]
tilesets: TilesetDict = {}
for raw_tileset in raw_tilesets:
if raw_tileset.get("source") is not None:
# Is an external Tileset
tileset_path = Path(parent_dir / raw_tileset["source"])
with open(tileset_path) as raw_tileset_file:
tilesets[raw_tileset["firstgid"]] = tileset.cast(
json.load(raw_tileset_file),
raw_tileset["firstgid"],
external_path=tileset_path.parent,
)
else:
# Is an embedded Tileset
raw_tileset = typing_cast(RawTileSet, raw_tileset)
tilesets[raw_tileset["firstgid"]] = tileset.cast(
raw_tileset, raw_tileset["firstgid"]
)
if isinstance(raw_tiled_map["version"], float):
version = str(raw_tiled_map["version"])
else:
version = raw_tiled_map["version"]
# `map` is a built-in function
map_ = TiledMap(
map_file=file,
infinite=raw_tiled_map["infinite"],
layers=[layer.cast(layer_, parent_dir) for layer_ in raw_tiled_map["layers"]],
map_size=Size(raw_tiled_map["width"], raw_tiled_map["height"]),
next_layer_id=raw_tiled_map["nextlayerid"],
next_object_id=raw_tiled_map["nextobjectid"],
orientation=raw_tiled_map["orientation"],
render_order=raw_tiled_map["renderorder"],
tiled_version=raw_tiled_map["tiledversion"],
tile_size=Size(raw_tiled_map["tilewidth"], raw_tiled_map["tileheight"]),
tilesets=tilesets,
version=version,
)
layers = [layer for layer in map_.layers if hasattr(layer, "tiled_objects")]
for my_layer in layers:
for tiled_object in my_layer.tiled_objects: # type: ignore
if hasattr(tiled_object, "new_tileset"):
if tiled_object.new_tileset:
already_loaded = None
for val in map_.tilesets.values():
if val.name == tiled_object.new_tileset["name"]:
already_loaded = val
break
if not already_loaded:
highest_firstgid = max(map_.tilesets.keys())
last_tileset_count = map_.tilesets[highest_firstgid].tile_count
new_firstgid = highest_firstgid + last_tileset_count
map_.tilesets[new_firstgid] = tileset.cast(
tiled_object.new_tileset,
new_firstgid,
tiled_object.new_tileset_path,
)
tiled_object.gid = tiled_object.gid + (new_firstgid - 1)
else:
tiled_object.gid = tiled_object.gid + (
already_loaded.firstgid - 1
)
tiled_object.new_tileset = None
tiled_object.new_tileset_path = None
if raw_tiled_map.get("backgroundcolor") is not None:
map_.background_color = parse_color(raw_tiled_map["backgroundcolor"])
if raw_tiled_map.get("hexsidelength") is not None:
map_.hex_side_length = raw_tiled_map["hexsidelength"]
if raw_tiled_map.get("properties") is not None:
map_.properties = properties.cast(raw_tiled_map["properties"])
if raw_tiled_map.get("staggeraxis") is not None:
map_.stagger_axis = raw_tiled_map["staggeraxis"]
if raw_tiled_map.get("staggerindex") is not None:
map_.stagger_index = raw_tiled_map["staggerindex"]
return map_

View File

@@ -1,14 +1,12 @@
# pylint: disable=too-few-public-methods
import json
import xml.etree.ElementTree as etree
from pathlib import Path
from typing import Any, Callable, Dict, List, Optional, Union
from typing import Any, Dict, List, Optional, Union
import attr
from typing_extensions import TypedDict
from . import properties as properties_
from .common_types import Color, OrderedPair, Size
from .util import parse_color
@attr.s(auto_attribs=True, kw_only=True)
@@ -37,10 +35,9 @@ class TiledObject:
coordinates: OrderedPair
size: Size = Size(0, 0)
rotation: float = 0
visible: bool
name: Optional[str] = None
type: Optional[str] = None
visible: bool = True
name: str = ""
type: str = ""
properties: properties_.Properties = {}
@@ -148,302 +145,5 @@ class Tile(TiledObject):
"""
gid: int
new_tileset: Optional[Dict[str, Any]] = None
new_tileset: Optional[Union[etree.Element, Dict[str, Any]]] = None
new_tileset_path: Optional[Path] = None
class RawTextDict(TypedDict):
""" The keys and their types that appear in a Text JSON Object."""
text: str
color: str
fontfamily: str
pixelsize: float # this is `font_size` in Text
bold: bool
italic: bool
strikeout: bool
underline: bool
kerning: bool
halign: str
valign: str
wrap: bool
class RawTiledObject(TypedDict):
""" The keys and their types that appear in a Tiled JSON Object."""
id: int
gid: int
template: str
x: float
y: float
width: float
height: float
rotation: float
visible: bool
name: str
type: str
properties: List[properties_.RawProperty]
ellipse: bool
point: bool
polygon: List[Dict[str, float]]
polyline: List[Dict[str, float]]
text: Dict[str, Union[float, str]]
RawTiledObjects = List[RawTiledObject]
def _get_common_attributes(raw_tiled_object: RawTiledObject) -> TiledObject:
"""Create a TiledObject containing all the attributes common to all tiled objects
Args:
raw_tiled_object: Raw Tiled object get common attributes from
Returns:
TiledObject: The attributes in common of all Tiled Objects
"""
common_attributes = TiledObject(
id=raw_tiled_object["id"],
coordinates=OrderedPair(raw_tiled_object["x"], raw_tiled_object["y"]),
visible=raw_tiled_object["visible"],
size=Size(raw_tiled_object["width"], raw_tiled_object["height"]),
rotation=raw_tiled_object["rotation"],
name=raw_tiled_object["name"],
type=raw_tiled_object["type"],
)
if raw_tiled_object.get("properties") is not None:
common_attributes.properties = properties_.cast(raw_tiled_object["properties"])
return common_attributes
def _cast_ellipse(raw_tiled_object: RawTiledObject) -> Ellipse:
"""Cast the raw_tiled_object to an Ellipse object.
Args:
raw_tiled_object: Raw Tiled object to be casted to an Ellipse
Returns:
Ellipse: The Ellipse object created from the raw_tiled_object
"""
return Ellipse(**_get_common_attributes(raw_tiled_object).__dict__)
def _cast_rectangle(raw_tiled_object: RawTiledObject) -> Rectangle:
"""Cast the raw_tiled_object to a Rectangle object.
Args:
raw_tiled_object: Raw Tiled object to be casted to a Rectangle
Returns:
Rectangle: The Rectangle object created from the raw_tiled_object
"""
return Rectangle(**_get_common_attributes(raw_tiled_object).__dict__)
def _cast_point(raw_tiled_object: RawTiledObject) -> Point:
"""Cast the raw_tiled_object to a Point object.
Args:
raw_tiled_object: Raw Tiled object to be casted to a Point
Returns:
Point: The Point object created from the raw_tiled_object
"""
return Point(**_get_common_attributes(raw_tiled_object).__dict__)
def _cast_tile(
raw_tiled_object: RawTiledObject,
new_tileset: Optional[Dict[str, Any]] = None,
new_tileset_path: Optional[Path] = None,
) -> Tile:
"""Cast the raw_tiled_object to a Tile object.
Args:
raw_tiled_object: Raw Tiled object to be casted to a Tile
Returns:
Tile: The Tile object created from the raw_tiled_object
"""
gid = raw_tiled_object["gid"]
return Tile(
gid=gid,
new_tileset=new_tileset,
new_tileset_path=new_tileset_path,
**_get_common_attributes(raw_tiled_object).__dict__
)
def _cast_polygon(raw_tiled_object: RawTiledObject) -> Polygon:
"""Cast the raw_tiled_object to a Polygon object.
Args:
raw_tiled_object: Raw Tiled object to be casted to a Polygon
Returns:
Polygon: The Polygon object created from the raw_tiled_object
"""
polygon = []
for point in raw_tiled_object["polygon"]:
polygon.append(OrderedPair(point["x"], point["y"]))
return Polygon(points=polygon, **_get_common_attributes(raw_tiled_object).__dict__)
def _cast_polyline(raw_tiled_object: RawTiledObject) -> Polyline:
"""Cast the raw_tiled_object to a Polyline object.
Args:
raw_tiled_object: Raw Tiled Object to be casted to a Polyline
Returns:
Polyline: The Polyline object created from the raw_tiled_object
"""
polyline = []
for point in raw_tiled_object["polyline"]:
polyline.append(OrderedPair(point["x"], point["y"]))
return Polyline(
points=polyline, **_get_common_attributes(raw_tiled_object).__dict__
)
def _cast_text(raw_tiled_object: RawTiledObject) -> Text:
"""Cast the raw_tiled_object to a Text object.
Args:
raw_tiled_object: Raw Tiled object to be casted to a Text object
Returns:
Text: The Text object created from the raw_tiled_object
"""
# required attributes
raw_text_dict: RawTextDict = raw_tiled_object["text"]
text = raw_text_dict["text"]
# create base Text object
text_object = Text(text=text, **_get_common_attributes(raw_tiled_object).__dict__)
# optional attributes
if raw_text_dict.get("color") is not None:
text_object.color = parse_color(raw_text_dict["color"])
if raw_text_dict.get("fontfamily") is not None:
text_object.font_family = raw_text_dict["fontfamily"]
if raw_text_dict.get("pixelsize") is not None:
text_object.font_size = raw_text_dict["pixelsize"]
if raw_text_dict.get("bold") is not None:
text_object.bold = raw_text_dict["bold"]
if raw_text_dict.get("italic") is not None:
text_object.italic = raw_text_dict["italic"]
if raw_text_dict.get("kerning") is not None:
text_object.kerning = raw_text_dict["kerning"]
if raw_text_dict.get("strikeout") is not None:
text_object.strike_out = raw_text_dict["strikeout"]
if raw_text_dict.get("underline") is not None:
text_object.underline = raw_text_dict["underline"]
if raw_text_dict.get("halign") is not None:
text_object.horizontal_align = raw_text_dict["halign"]
if raw_text_dict.get("valign") is not None:
text_object.vertical_align = raw_text_dict["valign"]
if raw_text_dict.get("wrap") is not None:
text_object.wrap = raw_text_dict["wrap"]
return text_object
def _get_caster(
raw_tiled_object: RawTiledObject,
) -> Callable[[RawTiledObject], TiledObject]:
"""Get the caster function for the raw tiled object.
Args:
raw_tiled_object: Raw Tiled object that is analysed to determine which caster
to return.
Returns:
Callable[[RawTiledObject], TiledObject]: The caster function.
"""
if raw_tiled_object.get("ellipse"):
return _cast_ellipse
if raw_tiled_object.get("point"):
return _cast_point
if raw_tiled_object.get("gid"):
# Only Tile objects have the `gid` key (I think)
return _cast_tile
if raw_tiled_object.get("polygon"):
return _cast_polygon
if raw_tiled_object.get("polyline"):
return _cast_polyline
if raw_tiled_object.get("text"):
return _cast_text
return _cast_rectangle
def cast(
raw_tiled_object: RawTiledObject,
parent_dir: Optional[Path] = None,
) -> TiledObject:
"""Cast the raw tiled object into a pytiled_parser type
Args:
raw_tiled_object: Raw Tiled object that is to be cast.
parent_dir: The parent directory that the map file is in.
Returns:
TiledObject: a properly typed Tiled object.
Raises:
RuntimeError: When a required parameter was not sent based on a condition.
"""
new_tileset = None
new_tileset_path = None
if raw_tiled_object.get("template"):
if not parent_dir:
raise RuntimeError(
"A parent directory must be specified when using object templates"
)
template_path = Path(parent_dir / raw_tiled_object["template"])
with open(template_path) as raw_template_file:
template = json.load(raw_template_file)
if "tileset" in template:
tileset_path = Path(
template_path.parent / template["tileset"]["source"]
)
with open(tileset_path) as raw_tileset_file:
new_tileset = json.load(raw_tileset_file)
new_tileset_path = tileset_path.parent
loaded_template = template["object"]
for key in loaded_template:
if key != "id":
raw_tiled_object[key] = loaded_template[key] # type: ignore
if raw_tiled_object.get("gid"):
return _cast_tile(raw_tiled_object, new_tileset, new_tileset_path)
return _get_caster(raw_tiled_object)(raw_tiled_object)

View File

@@ -1,16 +1,13 @@
# pylint: disable=too-few-public-methods
from pathlib import Path
from typing import Dict, List, NamedTuple, Optional, Union
from typing import Dict, List, NamedTuple, Optional
import attr
from typing_extensions import TypedDict
from . import layer
from . import properties as properties_
from .common_types import Color, OrderedPair
from .util import parse_color
from .wang_set import RawWangSet, WangSet
from .wang_set import cast as cast_wangset
from .wang_set import WangSet
class Grid(NamedTuple):
@@ -153,261 +150,3 @@ class Tileset:
properties: Optional[properties_.Properties] = None
tiles: Optional[Dict[int, Tile]] = None
wang_sets: Optional[List[WangSet]] = None
class RawFrame(TypedDict):
""" The keys and their types that appear in a Frame JSON Object."""
duration: int
tileid: int
class RawTileOffset(TypedDict):
""" The keys and their types that appear in a TileOffset JSON Object."""
x: int
y: int
class RawTransformations(TypedDict):
""" The keys and their types that appear in a Transformations JSON Object."""
hflip: bool
vflip: bool
rotate: bool
preferuntransformed: bool
class RawTile(TypedDict):
""" The keys and their types that appear in a Tile JSON Object."""
animation: List[RawFrame]
id: int
image: str
imageheight: int
imagewidth: int
opacity: float
properties: List[properties_.RawProperty]
objectgroup: layer.RawLayer
type: str
class RawGrid(TypedDict):
""" The keys and their types that appear in a Grid JSON Object."""
height: int
width: int
orientation: str
class RawTileSet(TypedDict):
""" The keys and their types that appear in a TileSet JSON Object."""
backgroundcolor: str
columns: int
firstgid: int
grid: RawGrid
image: str
imageheight: int
imagewidth: int
margin: int
name: str
properties: List[properties_.RawProperty]
source: str
spacing: int
tilecount: int
tiledversion: str
tileheight: int
tileoffset: RawTileOffset
tiles: List[RawTile]
tilewidth: int
transparentcolor: str
transformations: RawTransformations
version: Union[str, float]
wangsets: List[RawWangSet]
def _cast_frame(raw_frame: RawFrame) -> Frame:
"""Cast the raw_frame to a Frame.
Args:
raw_frame: RawFrame to be casted to a Frame
Returns:
Frame: The Frame created from the raw_frame
"""
return Frame(duration=raw_frame["duration"], tile_id=raw_frame["tileid"])
def _cast_tile_offset(raw_tile_offset: RawTileOffset) -> OrderedPair:
"""Cast the raw_tile_offset to an OrderedPair.
Args:
raw_tile_offset: RawTileOffset to be casted to an OrderedPair
Returns:
OrderedPair: The OrderedPair created from the raw_tile_offset
"""
return OrderedPair(raw_tile_offset["x"], raw_tile_offset["y"])
def _cast_tile(raw_tile: RawTile, external_path: Optional[Path] = None) -> Tile:
"""Cast the raw_tile to a Tile object.
Args:
raw_tile: RawTile to be casted to a Tile
Returns:
Tile: The Tile created from the raw_tile
"""
id_ = raw_tile["id"]
tile = Tile(id=id_)
if raw_tile.get("animation") is not None:
tile.animation = []
for frame in raw_tile["animation"]:
tile.animation.append(_cast_frame(frame))
if raw_tile.get("objectgroup") is not None:
tile.objects = layer.cast(raw_tile["objectgroup"])
if raw_tile.get("properties") is not None:
tile.properties = properties_.cast(raw_tile["properties"])
if raw_tile.get("image") is not None:
if external_path:
tile.image = Path(external_path / raw_tile["image"]).absolute().resolve()
else:
tile.image = Path(raw_tile["image"])
if raw_tile.get("imagewidth") is not None:
tile.image_width = raw_tile["imagewidth"]
if raw_tile.get("imageheight") is not None:
tile.image_height = raw_tile["imageheight"]
if raw_tile.get("type") is not None:
tile.type = raw_tile["type"]
return tile
def _cast_transformations(raw_transformations: RawTransformations) -> Transformations:
"""Cast the raw_transformations to a Transformations object.
Args:
raw_transformations: RawTransformations to be casted to a Transformations
Returns:
Transformations: The Transformations created from the raw_transformations
"""
return Transformations(
hflip=raw_transformations["hflip"],
vflip=raw_transformations["vflip"],
rotate=raw_transformations["rotate"],
prefer_untransformed=raw_transformations["preferuntransformed"],
)
def _cast_grid(raw_grid: RawGrid) -> Grid:
"""Cast the raw_grid to a Grid object.
Args:
raw_grid: RawGrid to be casted to a Grid
Returns:
Grid: The Grid created from the raw_grid
"""
return Grid(
orientation=raw_grid["orientation"],
width=raw_grid["width"],
height=raw_grid["height"],
)
def cast(
raw_tileset: RawTileSet,
firstgid: int,
external_path: Optional[Path] = None,
) -> Tileset:
"""Cast the raw tileset into a pytiled_parser type
Args:
raw_tileset: Raw Tileset to be cast.
firstgid: GID corresponding the first tile in the set.
external_path: The path to the tileset if it is not an embedded one.
Returns:
TileSet: a properly typed TileSet.
"""
tileset = Tileset(
name=raw_tileset["name"],
tile_count=raw_tileset["tilecount"],
tile_width=raw_tileset["tilewidth"],
tile_height=raw_tileset["tileheight"],
columns=raw_tileset["columns"],
spacing=raw_tileset["spacing"],
margin=raw_tileset["margin"],
firstgid=firstgid,
)
if raw_tileset.get("version") is not None:
if isinstance(raw_tileset["version"], float):
tileset.version = str(raw_tileset["version"])
else:
tileset.version = raw_tileset["version"]
if raw_tileset.get("tiledversion") is not None:
tileset.tiled_version = raw_tileset["tiledversion"]
if raw_tileset.get("image") is not None:
if external_path:
tileset.image = (
Path(external_path / raw_tileset["image"]).absolute().resolve()
)
else:
tileset.image = Path(raw_tileset["image"])
if raw_tileset.get("imagewidth") is not None:
tileset.image_width = raw_tileset["imagewidth"]
if raw_tileset.get("imageheight") is not None:
tileset.image_height = raw_tileset["imageheight"]
if raw_tileset.get("backgroundcolor") is not None:
tileset.background_color = parse_color(raw_tileset["backgroundcolor"])
if raw_tileset.get("tileoffset") is not None:
tileset.tile_offset = _cast_tile_offset(raw_tileset["tileoffset"])
if raw_tileset.get("transparentcolor") is not None:
tileset.transparent_color = parse_color(raw_tileset["transparentcolor"])
if raw_tileset.get("grid") is not None:
tileset.grid = _cast_grid(raw_tileset["grid"])
if raw_tileset.get("properties") is not None:
tileset.properties = properties_.cast(raw_tileset["properties"])
if raw_tileset.get("tiles") is not None:
tiles = {}
for raw_tile in raw_tileset["tiles"]:
tiles[raw_tile["id"]] = _cast_tile(raw_tile, external_path=external_path)
tileset.tiles = tiles
if raw_tileset.get("wangsets") is not None:
wangsets = []
for raw_wangset in raw_tileset["wangsets"]:
wangsets.append(cast_wangset(raw_wangset))
tileset.wang_sets = wangsets
if raw_tileset.get("transformations") is not None:
tileset.transformations = _cast_transformations(raw_tileset["transformations"])
return tileset

View File

@@ -1,4 +1,8 @@
"""Utility Functions for PyTiled"""
import json
import xml.etree.ElementTree as etree
from pathlib import Path
from typing import Any
from pytiled_parser.common_types import Color
@@ -27,3 +31,52 @@ def parse_color(color: str) -> Color:
)
raise ValueError("Improperly formatted color passed to parse_color")
def check_format(file_path: Path) -> str:
with open(file_path) as file:
line = file.readline().rstrip().strip()
if line[0] == "<":
return "tmx"
else:
return "json"
def load_object_template(file_path: Path) -> Any:
template_format = check_format(file_path)
new_tileset = None
new_tileset_path = None
if template_format == "tmx":
with open(file_path) as template_file:
template = etree.parse(template_file).getroot()
tileset_element = template.find("./tileset")
if tileset_element is not None:
tileset_path = Path(file_path.parent / tileset_element.attrib["source"])
new_tileset = load_object_tileset(tileset_path)
new_tileset_path = tileset_path.parent
elif template_format == "json":
with open(file_path) as template_file:
template = json.load(template_file)
if "tileset" in template:
tileset_path = Path(file_path.parent / template["tileset"]["source"]) # type: ignore
new_tileset = load_object_tileset(tileset_path)
new_tileset_path = tileset_path.parent
return (template, new_tileset, new_tileset_path)
def load_object_tileset(file_path: Path) -> Any:
tileset_format = check_format(file_path)
new_tileset = None
with open(file_path) as tileset_file:
if tileset_format == "tmx":
new_tileset = etree.parse(tileset_file).getroot()
elif tileset_format == "json":
new_tileset = json.load(tileset_file)
return new_tileset

View File

@@ -1,3 +1,3 @@
"""pytiled_parser version"""
__version__ = "1.5.4"
__version__ = "2.0.0-beta"

View File

@@ -1,11 +1,9 @@
from typing import Dict, List, Optional
import attr
from typing_extensions import TypedDict
from . import properties as properties_
from .common_types import Color
from .util import parse_color
from pytiled_parser.common_types import Color
from pytiled_parser.properties import Properties
@attr.s(auto_attribs=True)
@@ -22,7 +20,7 @@ class WangColor:
name: str
probability: float
tile: int
properties: Optional[properties_.Properties] = None
properties: Optional[Properties] = None
@attr.s(auto_attribs=True)
@@ -33,100 +31,4 @@ class WangSet:
wang_type: str
wang_tiles: Dict[int, WangTile]
wang_colors: List[WangColor]
properties: Optional[properties_.Properties] = None
class RawWangTile(TypedDict):
""" The keys and their types that appear in a Wang Tile JSON Object."""
tileid: int
# Tiled stores these IDs as a list represented like so:
# [top, top_right, right, bottom_right, bottom, bottom_left, left, top_left]
wangid: List[int]
class RawWangColor(TypedDict):
""" The keys and their types that appear in a Wang Color JSON Object."""
color: str
name: str
probability: float
tile: int
properties: List[properties_.RawProperty]
class RawWangSet(TypedDict):
""" The keys and their types that appear in a Wang Set JSON Object."""
colors: List[RawWangColor]
name: str
properties: List[properties_.RawProperty]
tile: int
type: str
wangtiles: List[RawWangTile]
def _cast_wang_tile(raw_wang_tile: RawWangTile) -> WangTile:
"""Cast the raw wang tile into a pytiled_parser type
Args:
raw_wang_tile: RawWangTile to be cast.
Returns:
WangTile: A properly typed WangTile.
"""
return WangTile(tile_id=raw_wang_tile["tileid"], wang_id=raw_wang_tile["wangid"])
def _cast_wang_color(raw_wang_color: RawWangColor) -> WangColor:
"""Cast the raw wang color into a pytiled_parser type
Args:
raw_wang_color: RawWangColor to be cast.
Returns:
WangColor: A properly typed WangColor.
"""
wang_color = WangColor(
name=raw_wang_color["name"],
color=parse_color(raw_wang_color["color"]),
tile=raw_wang_color["tile"],
probability=raw_wang_color["probability"],
)
if raw_wang_color.get("properties") is not None:
wang_color.properties = properties_.cast(raw_wang_color["properties"])
return wang_color
def cast(raw_wangset: RawWangSet) -> WangSet:
"""Cast the raw wangset into a pytiled_parser type
Args:
raw_wangset: Raw Wangset to be cast.
Returns:
WangSet: A properly typed WangSet.
"""
colors = []
for raw_wang_color in raw_wangset["colors"]:
colors.append(_cast_wang_color(raw_wang_color))
tiles = {}
for raw_wang_tile in raw_wangset["wangtiles"]:
tiles[raw_wang_tile["tileid"]] = _cast_wang_tile(raw_wang_tile)
wangset = WangSet(
name=raw_wangset["name"],
tile=raw_wangset["tile"],
wang_type=raw_wangset["type"],
wang_colors=colors,
wang_tiles=tiles,
)
if raw_wangset.get("properties") is not None:
wangset.properties = properties_.cast(raw_wangset["properties"])
return wangset
properties: Optional[Properties] = None

View File

@@ -8,8 +8,9 @@ from typing import List
import attr
from typing_extensions import TypedDict
from .common_types import OrderedPair, Size
from .tiled_map import TiledMap, parse_map
from pytiled_parser.common_types import OrderedPair, Size
from pytiled_parser.parser import parse_map
from pytiled_parser.tiled_map import TiledMap
@attr.s(auto_attribs=True)
@@ -55,7 +56,7 @@ class RawWorld(TypedDict):
onlyShowAdjacentMaps: bool
def _cast_world_map(raw_world_map: RawWorldMap, map_file: Path) -> WorldMap:
def _parse_world_map(raw_world_map: RawWorldMap, map_file: Path) -> WorldMap:
"""Parse the RawWorldMap into a WorldMap.
Args:
@@ -94,7 +95,7 @@ def parse_world(file: Path) -> World:
if raw_world.get("maps"):
for raw_map in raw_world["maps"]:
map_path = Path(parent_dir / raw_map["fileName"])
maps.append(_cast_world_map(raw_map, map_path))
maps.append(_parse_world_map(raw_map, map_path))
if raw_world.get("patterns"):
for raw_pattern in raw_world["patterns"]:
@@ -131,7 +132,7 @@ def parse_world(file: Path) -> World:
}
map_path = Path(parent_dir / map_file)
maps.append(_cast_world_map(raw_world_map, map_path))
maps.append(_parse_world_map(raw_world_map, map_path))
world = World(maps=maps)

View File

@@ -44,7 +44,7 @@ tests =
pytest
pytest-cov
black
pylint
flake8
mypy
isort<5,>=4.2.5
@@ -104,3 +104,7 @@ strict_optional = True
[mypy-tests.*]
ignore_errors = True
[flake8]
max-line-length = 88
exclude = .git,__pycache__,__init__.py,.mypy_cache,.pytest_cache

View File

@@ -0,0 +1,73 @@
{ "backgroundcolor":"#ff0004",
"compressionlevel":0,
"height":6,
"infinite":false,
"layers":[
{
"draworder":"topdown",
"id":2,
"name":"Object Layer 1",
"objects":[
{
"id":2,
"template":"template-rectangle.tx",
"x":98.4987608686521,
"y":46.2385012811358
}],
"opacity":1,
"type":"objectgroup",
"visible":true,
"x":0,
"y":0
}],
"nextlayerid":3,
"nextobjectid":8,
"orientation":"orthogonal",
"properties":[
{
"name":"bool property - true",
"type":"bool",
"value":true
},
{
"name":"color property",
"type":"color",
"value":"#ff49fcff"
},
{
"name":"file property",
"type":"file",
"value":"..\/..\/..\/..\/..\/..\/var\/log\/syslog"
},
{
"name":"float property",
"type":"float",
"value":1.23456789
},
{
"name":"int property",
"type":"int",
"value":13
},
{
"name":"string property",
"type":"string",
"value":"Hello, World!!"
}],
"renderorder":"right-down",
"tiledversion":"1.7.1",
"tileheight":32,
"tilesets":[
{
"firstgid":1,
"source":"tileset.json"
},
{
"firstgid":49,
"source":"tile_set_image_for_template.json"
}],
"tilewidth":32,
"type":"map",
"version":"1.6",
"width":8
}

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.5" tiledversion="1.7.0" orientation="orthogonal" renderorder="right-down" compressionlevel="0" width="8" height="6" tilewidth="32" tileheight="32" infinite="0" backgroundcolor="#ff0004" nextlayerid="3" nextobjectid="8">
<properties>
<property name="bool property - true" type="bool" value="true"/>
<property name="color property" type="color" value="#ff49fcff"/>
<property name="file property" type="file" value="../../../../../../var/log/syslog"/>
<property name="float property" type="float" value="1.23456789"/>
<property name="int property" type="int" value="13"/>
<property name="string property" value="Hello, World!!"/>
</properties>
<tileset firstgid="1" source="tileset.tsx"/>
<tileset firstgid="49" source="tile_set_image_for_template.tsx"/>
<objectgroup id="2" name="Object Layer 1">
<object id="2" template="template-rectangle.json" x="98.4988" y="46.2385"/>
</objectgroup>
</map>

View File

@@ -0,0 +1,12 @@
{ "object":
{
"height":38.2811778048473,
"id":1,
"name":"",
"rotation":0,
"type":"",
"visible":true,
"width":63.6585878103079
},
"type":"template"
}

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<template>
<object width="63.6586" height="38.2812"/>
</template>

View File

@@ -0,0 +1,16 @@
import os
from pathlib import Path
import pytest
from pytiled_parser import parse_map
def test_cross_template_tmx_json():
with pytest.raises(NotImplementedError):
parse_map(Path(os.path.dirname(os.path.abspath(__file__))) / "map.tmx")
def test_cross_template_json_tmx():
with pytest.raises(NotImplementedError):
parse_map(Path(os.path.dirname(os.path.abspath(__file__))) / "map.json")

View File

@@ -0,0 +1,14 @@
{ "columns":1,
"image":"..\/..\/images\/tile_04.png",
"imageheight":32,
"imagewidth":32,
"margin":0,
"name":"tile_set_image_for_template",
"spacing":0,
"tilecount":1,
"tiledversion":"1.7.1",
"tileheight":32,
"tilewidth":32,
"type":"tileset",
"version":"1.6"
}

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<tileset version="1.5" tiledversion="1.7.0" name="tile_set_image_for_template" tilewidth="32" tileheight="32" tilecount="1" columns="1">
<image source="../../images/tile_04.png" width="32" height="32"/>
</tileset>

View File

@@ -0,0 +1,14 @@
{ "columns":8,
"image":"..\/test_data\/images\/tmw_desert_spacing.png",
"imageheight":199,
"imagewidth":265,
"margin":1,
"name":"tile_set_image",
"spacing":1,
"tilecount":48,
"tiledversion":"1.6.0",
"tileheight":32,
"tilewidth":32,
"type":"tileset",
"version":"1.6"
}

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<tileset version="1.5" tiledversion="1.7.0" name="tile_set_image" tilewidth="32" tileheight="32" spacing="1" margin="1" tilecount="48" columns="8">
<image source="../../images/tmw_desert_spacing.png" width="265" height="199"/>
</tileset>

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.5" tiledversion="1.7.0" orientation="orthogonal" renderorder="right-down" width="8" height="6" tilewidth="32" tileheight="32" infinite="0" nextlayerid="6" nextobjectid="3">
<tileset firstgid="1" source="tileset.tsx"/>
<layer id="1" name="Tile Layer 1" width="8" height="6" tintcolor="#aaffff" offsetx="1" offsety="3" parallaxx="1.4" parallaxy="1.3">
<properties>
<property name="test" value="test property"/>
</properties>
<data encoding="csv">
1,2,3,4,5,6,7,8,
9,10,11,12,13,14,15,16,
17,18,19,20,21,22,23,24,
25,26,27,28,29,30,31,32,
33,34,35,36,37,38,39,40,
41,42,43,44,45,46,47,48
</data>
</layer>
<group id="4" name="Group 1" tintcolor="#0000ff" parallaxx="1.4">
<objectgroup id="2" name="Object Layer 1">
<object id="1" x="46.3333" y="39" width="69.3333" height="52.6667"/>
</objectgroup>
</group>
<imagelayer id="3" name="Image Layer 1" tintcolor="#ff0000" offsetx="1" offsety="4">
<image source="../../images/tile_04.png" trans="000000" width="32" height="32"/>
</imagelayer>
<imagelayer id="5" name="Image Layer 2" parallaxy="1.4">
<image source="../../images/tile_04.png" width="32" height="32"/>
</imagelayer>
</map>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<tileset version="1.5" tiledversion="1.7.0" name="tile_set_image" tilewidth="32" tileheight="32" spacing="1" margin="1" tilecount="48" columns="8">
<image source="../../images/tmw_desert_spacing.png" width="265" height="199"/>
</tileset>

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.5" tiledversion="1.7.0" orientation="orthogonal" renderorder="right-down" width="8" height="6" tilewidth="32" tileheight="32" infinite="0" nextlayerid="5" nextobjectid="3">
<tileset firstgid="1" source="../all_layer_types/tileset.tsx"/>
<layer id="1" name="Tile Layer 1" width="8" height="6">
<data encoding="base64">
AQAAAAIAAAADAAAABAAAAAUAAAAGAAAABwAAAAgAAAAJAAAACgAAAAsAAAAMAAAADQAAAA4AAAAPAAAAEAAAABEAAAASAAAAEwAAABQAAAAVAAAAFgAAABcAAAAYAAAAGQAAABoAAAAbAAAAHAAAAB0AAAAeAAAAHwAAACAAAAAhAAAAIgAAACMAAAAkAAAAJQAAACYAAAAnAAAAKAAAACkAAAAqAAAAKwAAACwAAAAtAAAALgAAAC8AAAAwAAAA
</data>
</layer>
<group id="4" name="Group 1">
<objectgroup id="2" name="Object Layer 1">
<object id="1" x="46.3333" y="39" width="69.3333" height="52.6667"/>
</objectgroup>
</group>
<imagelayer id="3" name="Image Layer 1">
<image source="../../images/tile_04.png" trans="000000" width="32" height="32"/>
</imagelayer>
</map>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<tileset version="1.5" tiledversion="1.7.0" name="tile_set_image" tilewidth="32" tileheight="32" spacing="1" margin="1" tilecount="48" columns="8">
<image source="../../images/tmw_desert_spacing.png" width="265" height="199"/>
</tileset>

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.5" tiledversion="1.7.0" orientation="orthogonal" renderorder="right-down" width="8" height="6" tilewidth="32" tileheight="32" infinite="0" nextlayerid="5" nextobjectid="3">
<tileset firstgid="1" source="../all_layer_types/tileset.tsx"/>
<layer id="1" name="Tile Layer 1" width="8" height="6">
<data encoding="base64" compression="gzip">
H4sIAAAAAAAACg3DBRKCQAAAwDMRA7BQLMTE9v+vY3dmWyGEth279uwbOTB26MixExNTM6fOnLtwae7KtYUbt+7ce7D0aOXJsxev3rxb+/Dpy7cfv/782wAcvDirwAAAAA==
</data>
</layer>
<group id="4" name="Group 1">
<objectgroup id="2" name="Object Layer 1">
<object id="1" x="46.3333" y="39" width="69.3333" height="52.6667"/>
</objectgroup>
</group>
<imagelayer id="3" name="Image Layer 1">
<image source="../../images/tile_04.png" trans="000000" width="32" height="32"/>
</imagelayer>
</map>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<tileset version="1.5" tiledversion="1.7.0" name="tile_set_image" tilewidth="32" tileheight="32" spacing="1" margin="1" tilecount="48" columns="8">
<image source="../../images/tmw_desert_spacing.png" width="265" height="199"/>
</tileset>

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.5" tiledversion="1.7.0" orientation="orthogonal" renderorder="right-down" width="8" height="6" tilewidth="32" tileheight="32" infinite="0" nextlayerid="5" nextobjectid="3">
<tileset firstgid="1" source="../all_layer_types/tileset.tsx"/>
<layer id="1" name="Tile Layer 1" width="8" height="6">
<data encoding="base64" compression="zlib">
eJwNwwUSgkAAAMAzEQOwUCzExPb/r2N3ZlshhLYdu/bsGzkwdujIsRMTUzOnzpy7cGnuyrWFG7fu3Huw9GjlybMXr968W/vw6cu3H7/+/NsAMw8EmQ==
</data>
</layer>
<group id="4" name="Group 1">
<objectgroup id="2" name="Object Layer 1">
<object id="1" x="46.3333" y="39" width="69.3333" height="52.6667"/>
</objectgroup>
</group>
<imagelayer id="3" name="Image Layer 1">
<image source="../../images/tile_04.png" trans="000000" width="32" height="32"/>
</imagelayer>
</map>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<tileset version="1.5" tiledversion="1.7.0" name="tile_set_image" tilewidth="32" tileheight="32" spacing="1" margin="1" tilecount="48" columns="8">
<image source="../../images/tmw_desert_spacing.png" width="265" height="199"/>
</tileset>

View File

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.5" tiledversion="1.7.0" orientation="orthogonal" renderorder="right-down" width="8" height="6" tilewidth="32" tileheight="32" infinite="1" nextlayerid="6" nextobjectid="3">
<editorsettings>
<chunksize width="4" height="8"/>
<export target="../tests/test_data/layer_tests/tests/test_data/layer_tests/tests/test_data/layer_tests/tests/test_data/layer_tests/tests/test_data/layer_tests/tests/test_data/layer_tests/tests/test_data/layer_tests/tests/test_data/layer_tests/tests/test_data/layer_tests/tests/test_data/layer_tests/tests/test_data/layer_tests/tests/test_data/layer_tests/tests/test_data/layer_tests/tests/test_data/layer_tests/tests/test_data/layer_tests/tests/test_data/layer_tests/tests/test_data/layer_tests/tests/test_data/layer_tests/all_layer_types"/>
</editorsettings>
<tileset firstgid="1" source="../all_layer_types/tileset.tsx"/>
<layer id="1" name="Tile Layer 1" width="16" height="16" offsetx="163.089" offsety="116.463">
<properties>
<property name="test" value="test property"/>
</properties>
<data encoding="csv">
<chunk x="0" y="0" width="4" height="8">
1,2,3,4,
9,10,11,12,
17,18,19,20,
25,26,27,28,
33,34,35,36,
41,42,43,44,
0,0,0,0,
0,0,0,0
</chunk>
<chunk x="4" y="0" width="4" height="8">
5,6,7,8,
13,14,15,16,
21,22,23,24,
29,30,31,32,
37,38,39,40,
45,46,47,48,
0,0,0,0,
0,0,0,0
</chunk>
</data>
</layer>
</map>

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.5" tiledversion="1.7.0" orientation="orthogonal" renderorder="right-down" width="8" height="6" tilewidth="32" tileheight="32" infinite="1" nextlayerid="6" nextobjectid="3">
<tileset firstgid="1" source="../all_layer_types/tileset.tsx"/>
<layer id="1" name="Tile Layer 1" width="16" height="16" offsetx="1" offsety="3">
<properties>
<property name="test" value="test property"/>
</properties>
<data encoding="base64">
<chunk x="0" y="0" width="16" height="16">
AQAAAAIAAAADAAAABAAAAAUAAAAGAAAABwAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAkAAAAKAAAACwAAAAwAAAANAAAADgAAAA8AAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARAAAAEgAAABMAAAAUAAAAFQAAABYAAAAXAAAAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGQAAABoAAAAbAAAAHAAAAB0AAAAeAAAAHwAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACEAAAAiAAAAIwAAACQAAAAlAAAAJgAAACcAAAAoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAApAAAAKgAAACsAAAAsAAAALQAAAC4AAAAvAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==
</chunk>
</data>
</layer>
</map>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.5" tiledversion="1.7.0" orientation="orthogonal" renderorder="right-down" compressionlevel="0" width="8" height="6" tilewidth="32" tileheight="32" infinite="0" nextlayerid="2" nextobjectid="1">
<properties>
<property name="bool property - false" type="bool" value="false"/>
<property name="bool property - true" type="bool" value="true"/>
<property name="color property" type="color" value="#ff49fcff"/>
<property name="file property" type="file" value="../tests/test_data/layer_tests/tests/test_data/layer_tests/tests/test_data/layer_tests/tests/test_data/layer_tests/tests/test_data/layer_tests/tests/test_data/layer_tests/tests/test_data/layer_tests/tests/test_data/layer_tests/tests/test_data/layer_tests/tests/test_data/layer_tests/tests/test_data/layer_tests/tests/test_data/layer_tests/tests/test_data/layer_tests/tests/test_data/layer_tests/tests/test_data/layer_tests/tests/test_data/layer_tests/tests/var/log/syslog"/>
<property name="float property" type="float" value="1.23456789"/>
<property name="int property" type="int" value="13"/>
<property name="string property" value="Hello, World!!"/>
</properties>
<tileset firstgid="1" source="tileset.tsx"/>
</map>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<tileset version="1.5" tiledversion="1.7.0" name="tile_set_image" tilewidth="32" tileheight="32" spacing="1" margin="1" tilecount="48" columns="8">
<image source="../../images/tmw_desert_spacing.png" width="265" height="199"/>
</tileset>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.5" tiledversion="1.7.0" orientation="orthogonal" renderorder="right-down" compressionlevel="0" width="8" height="6" tilewidth="32" tileheight="32" infinite="0" nextlayerid="2" nextobjectid="1">
<tileset firstgid="1" name="tileset" tilewidth="32" tileheight="32" spacing="1" margin="1" tilecount="48" columns="8">
<image source="../../images/tmw_desert_spacing.png" width="265" height="199"/>
</tileset>
</map>

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.5" tiledversion="1.7.0" orientation="orthogonal" renderorder="right-down" compressionlevel="0" width="8" height="6" tilewidth="32" tileheight="32" infinite="0" backgroundcolor="#ff0004" nextlayerid="3" nextobjectid="1">
<properties>
<property name="bool property - true" type="bool" value="true"/>
<property name="color property" type="color" value="#ff49fcff"/>
<property name="file property" type="file" value="../../../../../../var/log/syslog"/>
<property name="float property" type="float" value="1.23456789"/>
<property name="int property" type="int" value="13"/>
<property name="string property" value="Hello, World!!"/>
</properties>
<tileset firstgid="1" source="tileset/tileset.tsx"/>
<layer id="2" name="Layer 1" width="8" height="6">
<data encoding="base64" compression="zlib">
eAFjYWBgYAZiJiBmBOKhBgAIGAAL
</data>
</layer>
</map>

View File

@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<tileset version="1.5" tiledversion="1.7.0" name="tileset" tilewidth="32" tileheight="32" tilecount="4" columns="0">
<grid orientation="orthogonal" width="1" height="1"/>
<tile id="0" type="tile">
<properties>
<property name="float property" type="float" value="2.2"/>
</properties>
<image width="32" height="32" source="../../../images/tile_01.png"/>
<animation>
<frame tileid="0" duration="100"/>
<frame tileid="1" duration="100"/>
<frame tileid="2" duration="100"/>
<frame tileid="3" duration="100"/>
</animation>
</tile>
<tile id="1" type="tile">
<properties>
<property name="string property" value="testing"/>
</properties>
<image width="32" height="32" source="../../../images/tile_02.png"/>
<objectgroup draworder="index">
<object id="2" x="13.4358" y="13.5305" width="14.4766" height="13.7197"/>
<object id="3" x="13.8143" y="1.98699" width="14.2874" height="11.0704">
<ellipse/>
</object>
</objectgroup>
</tile>
<tile id="2" type="tile">
<properties>
<property name="bool property" type="bool" value="true"/>
</properties>
<image width="32" height="32" source="../../../images/tile_03.png"/>
</tile>
<tile id="3" type="tile">
<image width="32" height="32" source="../../../images/tile_04.png"/>
</tile>
</tileset>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.5" tiledversion="1.7.0" orientation="hexagonal" renderorder="right-down" width="10" height="10" tilewidth="14" tileheight="12" infinite="0" hexsidelength="6" staggeraxis="y" staggerindex="odd" nextlayerid="2" nextobjectid="1">
<tileset firstgid="1" source="tileset.tsx"/>
<layer id="1" name="Tile Layer 1" width="10" height="10">
<data encoding="csv">
3,3,3,3,9,9,9,9,17,17,
3,3,3,9,9,9,9,17,17,17,
3,3,3,9,9,9,9,9,17,17,
3,3,1,7,9,9,9,15,17,17,
1,1,12,5,7,7,7,15,15,15,
12,1,5,5,7,7,7,15,15,15,
2,2,5,5,5,5,4,14,14,14,
2,2,5,5,5,4,14,14,14,14,
2,2,2,5,5,5,4,14,14,14,
2,2,2,2,5,5,4,4,14,14
</data>
</layer>
</map>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<tileset version="1.5" tiledversion="1.7.0" name="tileset" tilewidth="18" tileheight="18" tilecount="20" columns="5">
<tileoffset x="0" y="1"/>
<image source="../../images/hexmini.png" width="106" height="72"/>
</tileset>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.6" tiledversion="1.6.0" orientation="orthogonal" renderorder="right-down" compressionlevel="0" width="8" height="6" tilewidth="32" tileheight="32" infinite="0" nextlayerid="2" nextobjectid="1">
<tileset firstgid="1" source="tileset.tsx"/>
</map>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<tileset version="1.6" tiledversion="1.6.0" name="tile_set_image" tilewidth="32" tileheight="32" spacing="1" margin="1" tilecount="48" columns="8">
<image source="../../images/tmw_desert_spacing.png" width="265" height="199"/>
</tileset>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.5" tiledversion="1.7.0" orientation="orthogonal" renderorder="right-down" compressionlevel="0" width="8" height="6" tilewidth="32" tileheight="32" infinite="0" backgroundcolor="#ff0004" nextlayerid="2" nextobjectid="1">
<properties>
<property name="bool property - true" type="bool" value="true"/>
<property name="color property" type="color" value="#ff49fcff"/>
<property name="file property" type="file" value="../../../../../../var/log/syslog"/>
<property name="float property" type="float" value="1.23456789"/>
<property name="int property" type="int" value="13"/>
<property name="string property" value="Hello, World!!"/>
</properties>
<tileset firstgid="1" source="tileset.tsx"/>
</map>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<tileset version="1.6" tiledversion="1.6.0" name="tile_set_image" tilewidth="32" tileheight="32" spacing="1" margin="1" tilecount="48" columns="8">
<image source="../../images/tmw_desert_spacing.png" width="265" height="199"/>
</tileset>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.5" tiledversion="1.7.0" orientation="orthogonal" renderorder="right-down" compressionlevel="0" width="8" height="6" tilewidth="32" tileheight="32" infinite="0" backgroundcolor="#ff0004" nextlayerid="3" nextobjectid="8">
<properties>
<property name="bool property - true" type="bool" value="true"/>
<property name="color property" type="color" value="#ff49fcff"/>
<property name="file property" type="file" value="../../../../../../var/log/syslog"/>
<property name="float property" type="float" value="1.23456789"/>
<property name="int property" type="int" value="13"/>
<property name="string property" value="Hello, World!!"/>
</properties>
<tileset firstgid="1" source="tileset.tsx"/>
<tileset firstgid="49" source="tile_set_image_for_template.tsx"/>
<objectgroup id="2" name="Object Layer 1">
<object id="2" template="template-rectangle.tx" x="98.4988" y="46.2385"/>
<object id="6" template="template-tile-spritesheet.tx" x="46" y="136.667"/>
<object id="7" template="template-tile-image.tx" x="141.333" y="148"/>
</objectgroup>
</map>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<template>
<object width="63.6586" height="38.2812"/>
</template>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<template>
<tileset firstgid="1" source="tile_set_single_image.tsx"/>
<object gid="1" width="32" height="32"/>
</template>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<template>
<tileset firstgid="1" source="tile_set_image_for_template.tsx"/>
<object gid="1" width="32" height="32"/>
</template>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<tileset version="1.5" tiledversion="1.7.0" name="tile_set_image_for_template" tilewidth="32" tileheight="32" tilecount="1" columns="1">
<image source="../../images/tile_04.png" width="32" height="32"/>
</tileset>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<tileset version="1.5" tiledversion="1.7.0" name="tile_set_single_image" tilewidth="32" tileheight="32" tilecount="1" columns="0">
<grid orientation="orthogonal" width="1" height="1"/>
<tile id="0">
<image width="32" height="32" source="../../images/tile_02.png"/>
</tile>
</tileset>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<tileset version="1.5" tiledversion="1.7.0" name="tile_set_image" tilewidth="32" tileheight="32" spacing="1" margin="1" tilecount="48" columns="8">
<image source="../../images/tmw_desert_spacing.png" width="265" height="199"/>
</tileset>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<tileset version="1.5" tiledversion="1.7.0" name="tile_set_image" tilewidth="32" tileheight="32" spacing="1" margin="1" tilecount="48" columns="8">
<image source="../../images/tmw_desert_spacing.png" width="265" height="199"/>
</tileset>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<tileset version="1.5" tiledversion="1.7.0" name="tile_set_image" tilewidth="32" tileheight="32" spacing="1" margin="1" tilecount="48" columns="8" backgroundcolor="#5500ff">
<image source="../../images/tmw_desert_spacing.png" width="265" height="199"/>
</tileset>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<tileset version="1.5" tiledversion="1.7.0" name="tile_set_image" tilewidth="32" tileheight="32" spacing="1" margin="1" tilecount="48" columns="8">
<editorsettings>
<export target="../tests/test_data/tilesets/tests/test_data/tilesets/tests/test_data/tilesets/tests/test_data/tilesets/tests/test_data/tilesets/tests/test_data/tilesets/tests/test_data/tilesets/tests/test_data/tilesets/tests/test_data/tilesets/tests/test_data/tilesets/tests/test_data/tilesets/tests/test_data/tilesets/tests/test_data/tilesets/tests/test_data/tilesets/tests/test_data/tilesets/tests/test_data/tilesets/tests/test_data/tilesets/tests/test_data/tilesets/image" format=""/>
</editorsettings>
<grid orientation="isometric" width="32" height="32"/>
<image source="../../images/tmw_desert_spacing.png" width="265" height="199"/>
</tileset>

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<tileset version="1.5" tiledversion="1.7.0" name="tile_set_image" tilewidth="32" tileheight="32" spacing="1" margin="1" tilecount="48" columns="8">
<editorsettings>
<export target="../tests/test_data/tilesets/tests/test_data/tilesets/tests/test_data/tilesets/tests/test_data/tilesets/tests/test_data/tilesets/tests/test_data/tilesets/tests/test_data/tilesets/tests/test_data/tilesets/tests/test_data/tilesets/tests/test_data/tilesets/tests/test_data/tilesets/tests/test_data/tilesets/tests/test_data/tilesets/tests/test_data/tilesets/tests/test_data/tilesets/tests/test_data/tilesets/tests/test_data/tilesets/tests/test_data/tilesets/image" format=""/>
</editorsettings>
<properties>
<property name="bool property" type="bool" value="true"/>
<property name="color property" type="color" value="#ff0000ff"/>
<property name="float property" type="float" value="5.6"/>
<property name="int property" type="int" value="5"/>
<property name="string property" value="testing"/>
</properties>
<image source="../../images/tmw_desert_spacing.png" width="265" height="199"/>
</tileset>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<tileset version="1.5" tiledversion="1.7.0" name="tile_set_image" tilewidth="32" tileheight="32" spacing="1" margin="1" tilecount="48" columns="8">
<editorsettings>
<export target="../tests/test_data/tilesets/tests/test_data/tilesets/tests/test_data/tilesets/tests/test_data/tilesets/tests/test_data/tilesets/tests/test_data/tilesets/tests/test_data/tilesets/tests/test_data/tilesets/tests/test_data/tilesets/tests/test_data/tilesets/tests/test_data/tilesets/tests/test_data/tilesets/tests/test_data/tilesets/tests/test_data/tilesets/tests/test_data/tilesets/tests/test_data/tilesets/tests/test_data/tilesets/tests/test_data/tilesets/image" format=""/>
</editorsettings>
<tileoffset x="3" y="5"/>
<image source="../../images/tmw_desert_spacing.png" width="265" height="199"/>
</tileset>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<tileset version="1.5" tiledversion="1.7.0" name="tile_set_image" tilewidth="32" tileheight="32" spacing="1" margin="1" tilecount="48" columns="8">
<transformations hflip="1" vflip="0" rotate="0" preferuntransformed="0"/>
<image source="../../images/tmw_desert_spacing.png" width="265" height="199"/>
</tileset>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<tileset version="1.5" tiledversion="1.7.0" name="tileset" tilewidth="32" tileheight="32" spacing="1" margin="1" tilecount="48" columns="8">
<image source="../../images/tmw_desert_spacing.png" trans="ff00ff" width="265" height="199"/>
</tileset>

View File

@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<tileset version="1.5" tiledversion="1.7.0" name="tileset" tilewidth="32" tileheight="32" tilecount="4" columns="0">
<grid orientation="orthogonal" width="1" height="1"/>
<tile id="0" type="tile">
<properties>
<property name="float property" type="float" value="2.2"/>
</properties>
<image width="32" height="32" source="../../images/tile_01.png"/>
<animation>
<frame tileid="0" duration="100"/>
<frame tileid="1" duration="100"/>
<frame tileid="2" duration="100"/>
<frame tileid="3" duration="100"/>
</animation>
</tile>
<tile id="1" type="tile">
<properties>
<property name="string property" value="testing"/>
</properties>
<image width="32" height="32" source="../../images/tile_02.png"/>
<objectgroup draworder="index">
<object id="2" x="13.4358" y="13.5305" width="14.4766" height="13.7197"/>
<object id="3" x="13.8143" y="1.98699" width="14.2874" height="11.0704">
<ellipse/>
</object>
</objectgroup>
</tile>
<tile id="2" type="tile">
<properties>
<property name="bool property" type="bool" value="true"/>
</properties>
<image width="32" height="32" source="../../images/tile_03.png"/>
</tile>
<tile id="3" type="tile">
<image width="32" height="32" source="../../images/tile_04.png"/>
</tile>
</tileset>

View File

@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8"?>
<tileset version="1.5" tiledversion="1.7.0" name="tileset" tilewidth="32" tileheight="32" spacing="1" margin="1" tilecount="48" columns="8">
<image source="../../images/tmw_desert_spacing.png" width="265" height="199"/>
<wangsets>
<wangset name="Terrains" type="mixed" tile="-1">
<wangcolor name="Sand" color="#ff0000" tile="-1" probability="1"/>
<wangcolor name="Cobblestone" color="#00ff00" tile="-1" probability="1"/>
<wangcolor name="Pavement" color="#0000ff" tile="-1" probability="1"/>
<wangcolor name="Dirt" color="#ff7700" tile="-1" probability="1"/>
<wangtile tileid="0" wangid="1,1,0,2,0,1,1,1"/>
<wangtile tileid="1" wangid="1,1,0,2,2,2,0,1"/>
<wangtile tileid="2" wangid="1,1,1,1,0,2,0,1"/>
<wangtile tileid="3" wangid="4,4,0,1,0,4,4,4"/>
<wangtile tileid="4" wangid="4,4,4,4,0,1,0,4"/>
<wangtile tileid="5" wangid="1,1,0,4,0,1,1,1"/>
<wangtile tileid="6" wangid="1,1,0,4,4,4,0,1"/>
<wangtile tileid="7" wangid="1,1,1,1,0,4,0,1"/>
<wangtile tileid="8" wangid="0,2,2,2,0,1,1,1"/>
<wangtile tileid="9" wangid="2,2,2,2,2,2,2,2"/>
<wangtile tileid="10" wangid="0,1,1,1,0,2,2,2"/>
<wangtile tileid="11" wangid="0,1,0,4,4,4,4,4"/>
<wangtile tileid="12" wangid="0,4,4,4,4,4,0,1"/>
<wangtile tileid="13" wangid="0,4,4,4,0,1,1,1"/>
<wangtile tileid="14" wangid="4,4,4,4,4,4,4,4"/>
<wangtile tileid="15" wangid="0,1,1,1,0,4,4,4"/>
<wangtile tileid="16" wangid="0,2,0,1,1,1,1,1"/>
<wangtile tileid="17" wangid="2,2,0,1,1,1,0,2"/>
<wangtile tileid="18" wangid="0,1,1,1,1,1,0,2"/>
<wangtile tileid="19" wangid="2,2,0,1,0,2,2,2"/>
<wangtile tileid="20" wangid="2,2,2,2,0,1,0,2"/>
<wangtile tileid="21" wangid="0,4,0,1,1,1,1,1"/>
<wangtile tileid="22" wangid="4,4,0,1,1,1,0,4"/>
<wangtile tileid="23" wangid="0,1,1,1,1,1,0,4"/>
<wangtile tileid="24" wangid="1,1,0,3,0,1,1,1"/>
<wangtile tileid="25" wangid="1,1,0,3,3,3,0,1"/>
<wangtile tileid="26" wangid="1,1,1,1,0,3,0,1"/>
<wangtile tileid="27" wangid="0,1,0,2,2,2,2,2"/>
<wangtile tileid="28" wangid="0,2,2,2,2,2,0,1"/>
<wangtile tileid="29" wangid="1,1,1,1,1,1,1,1"/>
<wangtile tileid="32" wangid="0,3,3,3,0,1,1,1"/>
<wangtile tileid="33" wangid="3,3,3,3,3,3,3,3"/>
<wangtile tileid="34" wangid="0,1,1,1,0,3,3,3"/>
<wangtile tileid="35" wangid="3,3,0,1,0,3,3,3"/>
<wangtile tileid="36" wangid="3,3,3,3,0,1,0,3"/>
<wangtile tileid="40" wangid="0,3,0,1,1,1,1,1"/>
<wangtile tileid="41" wangid="3,3,0,1,1,1,0,3"/>
<wangtile tileid="42" wangid="0,1,1,1,1,1,0,3"/>
<wangtile tileid="43" wangid="0,1,0,3,3,3,3,3"/>
<wangtile tileid="44" wangid="0,3,3,3,3,3,0,1"/>
</wangset>
</wangsets>
</tileset>

View File

@@ -2,11 +2,14 @@
import importlib.util
import json
import os
import xml.etree.ElementTree as etree
from pathlib import Path
import pytest
from pytiled_parser import layer
from pytiled_parser.common_types import OrderedPair, Size
from pytiled_parser.parsers.json.layer import parse as parse_json
from pytiled_parser.parsers.tmx.layer import parse as parse_tmx
TESTS_DIR = Path(os.path.dirname(os.path.abspath(__file__)))
TEST_DATA = TESTS_DIR / "test_data"
@@ -25,8 +28,36 @@ ALL_LAYER_TESTS = [
]
def fix_object(my_object):
my_object.coordinates = OrderedPair(
round(my_object.coordinates[0], 4),
round(my_object.coordinates[1], 4),
)
my_object.size = Size(round(my_object.size[0], 4), round(my_object.size[1], 4))
def fix_layer(layer):
layer.offset = OrderedPair(round(layer.offset[0], 3), round(layer.offset[1], 3))
layer.coordinates = OrderedPair(
round(layer.coordinates[0], 4), round(layer.coordinates[1], 4)
)
if layer.size:
layer.size = Size(round(layer.size[0], 4), round(layer.size[1], 4))
layer.parallax_factor = OrderedPair(
round(layer.parallax_factor[0], 4),
round(layer.parallax_factor[1], 4),
)
if hasattr(layer, "tiled_objects"):
for tiled_object in layer.tiled_objects:
fix_object(tiled_object)
if hasattr(layer, "layers"):
for child_layer in layer.layers:
fix_layer(child_layer)
@pytest.mark.parametrize("parser_type", ["json", "tmx"])
@pytest.mark.parametrize("layer_test", ALL_LAYER_TESTS)
def test_layer_integration(layer_test):
def test_layer_integration(parser_type, layer_test):
# it's a PITA to import like this, don't do it
# https://stackoverflow.com/a/67692/1342874
spec = importlib.util.spec_from_file_location(
@@ -35,10 +66,33 @@ def test_layer_integration(layer_test):
expected = importlib.util.module_from_spec(spec)
spec.loader.exec_module(expected)
raw_layers_path = layer_test / "map.json"
if parser_type == "json":
raw_layers_path = layer_test / "map.json"
with open(raw_layers_path) as raw_layers_file:
raw_layers = json.load(raw_layers_file)["layers"]
layers = [parse_json(raw_layer) for raw_layer in raw_layers]
elif parser_type == "tmx":
raw_layers_path = layer_test / "map.tmx"
with open(raw_layers_path) as raw_layers_file:
raw_layer = etree.parse(raw_layers_file).getroot()
layers = []
for layer in raw_layer.findall("./layer"):
layers.append(parse_tmx(layer))
with open(raw_layers_path) as raw_layers_file:
raw_layers = json.load(raw_layers_file)["layers"]
layers = [layer.cast(raw_layer) for raw_layer in raw_layers]
for layer in raw_layer.findall("./objectgroup"):
layers.append(parse_tmx(layer))
for layer in raw_layer.findall("./group"):
layers.append(parse_tmx(layer))
for layer in raw_layer.findall("./imagelayer"):
layers.append(parse_tmx(layer))
for layer in layers:
fix_layer(layer)
for layer in expected.EXPECTED:
fix_layer(layer)
print(layer.size)
assert layers == expected.EXPECTED

View File

@@ -5,7 +5,8 @@ from pathlib import Path
import pytest
from pytiled_parser import tiled_map
from pytiled_parser import parse_map
from pytiled_parser.common_types import OrderedPair, Size
TESTS_DIR = Path(os.path.dirname(os.path.abspath(__file__)))
TEST_DATA = TESTS_DIR / "test_data"
@@ -21,17 +22,64 @@ ALL_MAP_TESTS = [
]
def fix_object(my_object):
my_object.coordinates = OrderedPair(
round(my_object.coordinates[0], 3), round(my_object.coordinates[1], 3)
)
my_object.size = Size(round(my_object.size[0], 4), round(my_object.size[1], 4))
def fix_tileset(tileset):
tileset.version = None
tileset.tiled_version = None
if tileset.tiles:
for tile in tileset.tiles.values():
if tile.objects:
for my_object in tile.objects.tiled_objects:
fix_object(my_object)
def fix_layer(layer):
for tiled_object in layer.tiled_objects:
fix_object(tiled_object)
def fix_map(map):
map.version = None
map.tiled_version = None
for layer in [layer for layer in map.layers if hasattr(layer, "tiled_objects")]:
fix_layer(layer)
for tileset in map.tilesets.values():
fix_tileset(tileset)
@pytest.mark.parametrize("parser_type", ["json", "tmx"])
@pytest.mark.parametrize("map_test", ALL_MAP_TESTS)
def test_map_integration(map_test):
def test_map_integration(parser_type, map_test):
# it's a PITA to import like this, don't do it
# https://stackoverflow.com/a/67692/1342874
spec = importlib.util.spec_from_file_location("expected", map_test / "expected.py")
expected = importlib.util.module_from_spec(spec)
spec.loader.exec_module(expected)
raw_maps_path = map_test / "map.json"
if parser_type == "json":
raw_maps_path = map_test / "map.json"
elif parser_type == "tmx":
raw_maps_path = map_test / "map.tmx"
casted_map = tiled_map.parse_map(raw_maps_path)
casted_map = parse_map(raw_maps_path)
# file detection when running from unit tests is broken
expected.EXPECTED.map_file = casted_map.map_file
# who even knows what/how/when the gods determine what the
# version values in maps/tileset files are, so we're just not
# gonna check them, because they don't make sense anyways.
#
# Yes the values could be set to None in the expected objects
# directly, but alas, this is just test code that's already stupid fast
# and I'm lazy because there's too many of them already existing.
fix_map(expected.EXPECTED)
fix_map(casted_map)
assert casted_map == expected.EXPECTED

View File

@@ -5,7 +5,17 @@ from pathlib import Path
import pytest
from pytiled_parser import common_types, tiled_object
from pytiled_parser import common_types
from pytiled_parser.parsers.json.tiled_object import parse
from pytiled_parser.tiled_object import (
Ellipse,
Point,
Polygon,
Polyline,
Rectangle,
Text,
Tile,
)
ELLIPSES = [
(
@@ -23,7 +33,7 @@ ELLIPSES = [
"y":81.1913152210981
}
""",
tiled_object.Ellipse(
Ellipse(
id=6,
size=common_types.Size(57.4013868364215, 18.5517790155735),
name="name: ellipse",
@@ -48,7 +58,7 @@ ELLIPSES = [
"y":53.9092872570194
}
""",
tiled_object.Ellipse(
Ellipse(
id=7,
size=common_types.Size(6.32943048766625, 31.4288962146186),
name="name: ellipse - invisible",
@@ -73,7 +83,7 @@ ELLIPSES = [
"y":120.040923041946
}
""",
tiled_object.Ellipse(
Ellipse(
id=8,
size=common_types.Size(29.6828464249176, 24.2264408321018),
name="name: ellipse - rotated",
@@ -98,7 +108,7 @@ ELLIPSES = [
"y":127.679890871888
}
""",
tiled_object.Ellipse(
Ellipse(
id=29,
name="name: ellipse - no width or height",
rotation=0,
@@ -124,7 +134,7 @@ RECTANGLES = [
"y":23.571672160964
}
""",
tiled_object.Rectangle(
Rectangle(
id=1,
size=common_types.Size(45.3972945322269, 41.4686825053996),
name="name: rectangle",
@@ -148,7 +158,7 @@ RECTANGLES = [
"y":91.0128452881664
}
""",
tiled_object.Rectangle(
Rectangle(
id=4,
size=common_types.Size(30.9923837671934, 32.7384335568944),
name="name: rectangle - invisible",
@@ -172,7 +182,7 @@ RECTANGLES = [
"y":23.3534159372513
}
""",
tiled_object.Rectangle(
Rectangle(
id=5,
size=common_types.Size(10, 22),
name="name: rectangle - rotated",
@@ -196,7 +206,7 @@ RECTANGLES = [
"y":53.4727748095942
}
""",
tiled_object.Rectangle(
Rectangle(
id=28,
size=common_types.Size(0, 0),
name="name: rectangle - no width or height",
@@ -251,7 +261,7 @@ RECTANGLES = [
"y":131.826759122428
}
""",
tiled_object.Rectangle(
Rectangle(
id=30,
size=common_types.Size(21.170853700125, 13.7501420938956),
name="name: rectangle - properties",
@@ -287,7 +297,7 @@ POINTS = [
"y":82.9373650107991
}
""",
tiled_object.Point(
Point(
id=2,
name="name: point",
rotation=0,
@@ -311,7 +321,7 @@ POINTS = [
"y":95.8144822098443
}
""",
tiled_object.Point(
Point(
id=3,
name="name: point invisible",
rotation=0,
@@ -338,7 +348,7 @@ TILES = [
"y":48.3019211094691
}
""",
tiled_object.Tile(
Tile(
id=13,
size=common_types.Size(32, 32),
name="name: tile",
@@ -364,7 +374,7 @@ TILES = [
"y":168.779356598841
}
""",
tiled_object.Tile(
Tile(
id=14,
size=common_types.Size(32, 32),
name="name: tile - invisible",
@@ -390,7 +400,7 @@ TILES = [
"y":59.8695009662385
}
""",
tiled_object.Tile(
Tile(
id=15,
size=common_types.Size(32, 32),
name="name: tile - horizontal flipped",
@@ -416,7 +426,7 @@ TILES = [
"y":60.742525861089
}
""",
tiled_object.Tile(
Tile(
id=16,
size=common_types.Size(32, 32),
name="name: tile - vertical flipped",
@@ -442,7 +452,7 @@ TILES = [
"y":95.6635216551097
}
""",
tiled_object.Tile(
Tile(
id=17,
size=common_types.Size(32, 32),
name="name: tile - both flipped",
@@ -468,7 +478,7 @@ TILES = [
"y":142.62
}
""",
tiled_object.Tile(
Tile(
id=18,
size=common_types.Size(32, 32),
name="name: tile - rotated",
@@ -517,7 +527,7 @@ POLYGONS = [
"y":38.6313515971354
}
""",
tiled_object.Polygon(
Polygon(
id=9,
name="name: polygon",
points=[
@@ -560,7 +570,7 @@ POLYGONS = [
"y":24.4446970558145
}
""",
tiled_object.Polygon(
Polygon(
id=10,
name="name: polygon - invisible",
points=[
@@ -613,7 +623,7 @@ POLYGONS = [
"y":19.8613163578493
}
""",
tiled_object.Polygon(
Polygon(
id=11,
name="name: polygon - rotated",
points=[
@@ -660,7 +670,7 @@ POLYLINES = [
"y":90.1398203933159
}
""",
tiled_object.Polyline(
Polyline(
id=12,
name="name: polyline",
points=[
@@ -701,7 +711,7 @@ POLYLINES = [
"y":163.333333333333
}
""",
tiled_object.Polyline(
Polyline(
id=31,
name="name: polyline - invisible",
points=[
@@ -742,7 +752,7 @@ POLYLINES = [
"y":128.666666666667
}
""",
tiled_object.Polyline(
Polyline(
id=32,
name="name: polyline - rotated",
points=[
@@ -778,7 +788,7 @@ TEXTS = [
"y":93.2986813686484
}
""",
tiled_object.Text(
Text(
id=19,
name="name: text",
text="Hello World",
@@ -809,7 +819,7 @@ TEXTS = [
"y":112.068716607935
}
""",
tiled_object.Text(
Text(
id=20,
name="name: text - invisible",
text="Hello World",
@@ -840,7 +850,7 @@ TEXTS = [
"y":78.4572581561896
}
""",
tiled_object.Text(
Text(
id=21,
name="name: text - rotated",
text="Hello World",
@@ -874,7 +884,7 @@ TEXTS = [
"y":101.592417869728
}
""",
tiled_object.Text(
Text(
id=22,
name="name: text - different font",
text="Hello World",
@@ -907,7 +917,7 @@ TEXTS = [
"y":154.192167784472
}
""",
tiled_object.Text(
Text(
id=23,
name="name: text - no word wrap",
text="Hello World",
@@ -939,7 +949,7 @@ TEXTS = [
"y":1.19455496191883
}
""",
tiled_object.Text(
Text(
id=24,
name="name: text - right bottom align",
text="Hello World",
@@ -973,7 +983,7 @@ TEXTS = [
"y": 3.81362964647039
}
""",
tiled_object.Text(
Text(
id=25,
name="text: center center align",
rotation=0,
@@ -1006,7 +1016,7 @@ TEXTS = [
"y": 60.7785040354666
}
""",
tiled_object.Text(
Text(
id=26,
name="name: text - justified",
rotation=0,
@@ -1038,7 +1048,7 @@ TEXTS = [
"y": 130.620495623508
}
""",
tiled_object.Text(
Text(
id=27,
name="name: text - red",
rotation=0,
@@ -1075,7 +1085,7 @@ TEXTS = [
"y":22
}
""",
tiled_object.Text(
Text(
id=31,
name="name: text - font options",
rotation=0,
@@ -1100,7 +1110,7 @@ OBJECTS = ELLIPSES + RECTANGLES + POINTS + TILES + POLYGONS + POLYLINES + TEXTS
@pytest.mark.parametrize("raw_object_json,expected", OBJECTS)
def test_parse_layer(raw_object_json, expected):
raw_object = json.loads(raw_object_json)
result = tiled_object.cast(raw_object)
result = parse(raw_object)
assert result == expected
@@ -1118,4 +1128,4 @@ def test_parse_no_parent_dir():
json_object = json.loads(raw_object)
with pytest.raises(RuntimeError):
tiled_object.cast(json_object)
parse(json_object)

View File

@@ -0,0 +1,492 @@
"""Tests for objects"""
import xml.etree.ElementTree as etree
from contextlib import ExitStack as does_not_raise
from pathlib import Path
import pytest
from pytiled_parser import common_types
from pytiled_parser.parsers.tmx.tiled_object import parse
from pytiled_parser.tiled_object import (
Ellipse,
Point,
Polygon,
Polyline,
Rectangle,
Text,
Tile,
)
ELLIPSES = [
(
"""
<object id="6" x="37.5401" y="81.1913" width="57.4014" height="18.5518" name="ellipse">
<ellipse/>
</object>
""",
Ellipse(
id=6,
size=common_types.Size(57.4014, 18.5518),
name="ellipse",
coordinates=common_types.OrderedPair(37.5401, 81.1913),
),
),
(
"""
<object id="7" x="22.6986" y="53.9093" width="6.3294" height="31.4289" name="ellipse - invisible" visible="0">
<ellipse/>
</object>
""",
Ellipse(
id=7,
size=common_types.Size(6.3294, 31.4289),
name="ellipse - invisible",
visible=False,
coordinates=common_types.OrderedPair(22.6986, 53.9093),
),
),
(
"""
<object id="8" x="35.7940" y="120.0409" width="29.6828" height="24.2264" name="ellipse - rotated" rotation="111">
<ellipse/>
</object>
""",
Ellipse(
id=8,
size=common_types.Size(29.6828, 24.2264),
name="ellipse - rotated",
rotation=111,
coordinates=common_types.OrderedPair(35.7940, 120.0409),
),
),
(
"""
<object id="29" x="72.4611" y="127.6799" name="ellipse - no width or height">
<ellipse/>
</object>
""",
Ellipse(
id=29,
name="ellipse - no width or height",
coordinates=common_types.OrderedPair(72.4611, 127.6799),
),
),
]
RECTANGLES = [
(
"""
<object id="1" x="27.7185" y="23.5717" width="45.3973" height="41.4687" name="rectangle"/>
""",
Rectangle(
id=1,
size=common_types.Size(45.3973, 41.4687),
coordinates=common_types.OrderedPair(27.7185, 23.5717),
name="rectangle",
),
),
(
"""
<object id="4" x="163.9104" y="91.0128" width="30.9924" height="32.7384" name="rectangle - invisible" visible="0"/>
""",
Rectangle(
id=4,
size=common_types.Size(30.9924, 32.7384),
coordinates=common_types.OrderedPair(163.9104, 91.0128),
name="rectangle - invisible",
visible=False,
),
),
(
"""
<object id="5" x="183.3352" y="23.3534" width="10" height="22" name="rectangle - rotated" rotation="10"/>
""",
Rectangle(
id=5,
size=common_types.Size(10, 22),
coordinates=common_types.OrderedPair(183.3352, 23.3534),
name="rectangle - rotated",
rotation=10,
),
),
(
"""
<object id="28" x="131.1720" y="53.4728" name="rectangle - no width or height"/>
""",
Rectangle(
id=28,
coordinates=common_types.OrderedPair(131.1720, 53.4728),
name="rectangle - no width or height",
),
),
(
r"""
<object id="30" x="39.0679" y="131.8268" width="21.1709" height="13.7501" name="rectangle - properties">
<properties>
<property name="bool property" type="bool" value="false"/>
<property name="color property" type="color" value="#ffaa0000"/>
<property name="file property" type="file" value="..\/..\/..\/..\/..\/..\/dev\/null"/>
<property name="float property" type="float" value="42.1"/>
<property name="int property" type="int" value="8675309"/>
<property name="string property" value="pytiled_parser rulez!1!!"/>
</properties>
</object>
""",
Rectangle(
id=30,
size=common_types.Size(21.1709, 13.7501),
coordinates=common_types.OrderedPair(39.0679, 131.8268),
name="rectangle - properties",
properties={
"bool property": False,
"color property": common_types.Color(170, 0, 0, 255),
"file property": Path("../../../../../../dev/null"),
"float property": 42.1,
"int property": 8675309,
"string property": "pytiled_parser rulez!1!!",
},
),
),
]
POINTS = [
(
"""
<object id="2" x="159.9818" y="82.9374" name="point">
<point/>
</object>
""",
Point(
id=2, coordinates=common_types.OrderedPair(159.9818, 82.9374), name="point"
),
),
(
"""
<object id="2" x="159.9818" y="82.9374" name="point - invisible" visible="0">
<point/>
</object>
""",
Point(
id=2,
coordinates=common_types.OrderedPair(159.9818, 82.9374),
name="point - invisible",
visible=False,
),
),
]
POLYGONS = [
(
"""
<object id="9" x="89.4851" y="38.6314" name="polygon">
<polygon points="0,0 19.4248,27.0638 19.6431,3.0556 -2.6191,15.9327 25.3177,16.3692"/>
</object>
""",
Polygon(
id=9,
coordinates=common_types.OrderedPair(89.4851, 38.6314),
name="polygon",
points=[
common_types.OrderedPair(0, 0),
common_types.OrderedPair(19.4248, 27.0638),
common_types.OrderedPair(19.6431, 3.0556),
common_types.OrderedPair(-2.6191, 15.9327),
common_types.OrderedPair(25.3177, 16.3692),
],
),
),
(
"""
<object id="9" x="89.4851" y="38.6314" name="polygon - invisible" visible="0">
<polygon points="0,0 19.4248,27.0638 19.6431,3.0556 -2.6191,15.9327 25.3177,16.3692"/>
</object>
""",
Polygon(
id=9,
coordinates=common_types.OrderedPair(89.4851, 38.6314),
name="polygon - invisible",
points=[
common_types.OrderedPair(0, 0),
common_types.OrderedPair(19.4248, 27.0638),
common_types.OrderedPair(19.6431, 3.0556),
common_types.OrderedPair(-2.6191, 15.9327),
common_types.OrderedPair(25.3177, 16.3692),
],
visible=False,
),
),
(
"""
<object id="9" x="89.4851" y="38.6314" name="polygon - rotated" rotation="123">
<polygon points="0,0 19.4248,27.0638 19.6431,3.0556 -2.6191,15.9327 25.3177,16.3692"/>
</object>
""",
Polygon(
id=9,
coordinates=common_types.OrderedPair(89.4851, 38.6314),
name="polygon - rotated",
points=[
common_types.OrderedPair(0, 0),
common_types.OrderedPair(19.4248, 27.0638),
common_types.OrderedPair(19.6431, 3.0556),
common_types.OrderedPair(-2.6191, 15.9327),
common_types.OrderedPair(25.3177, 16.3692),
],
rotation=123,
),
),
]
POLYLINES = [
(
"""
<object id="12" x="124.1878" y="90.1398" name="polyline">
<polyline points="0,0 -13.3136,41.0321 21.3891,16.8057"/>
</object>
""",
Polyline(
id=12,
coordinates=common_types.OrderedPair(124.1878, 90.1398),
name="polyline",
points=[
common_types.OrderedPair(0, 0),
common_types.OrderedPair(-13.3136, 41.0321),
common_types.OrderedPair(21.3891, 16.8057),
],
),
),
(
"""
<object id="12" x="124.1878" y="90.1398" name="polyline - invisible" visible="0">
<polyline points="0,0 -13.3136,41.0321 21.3891,16.8057"/>
</object>
""",
Polyline(
id=12,
coordinates=common_types.OrderedPair(124.1878, 90.1398),
name="polyline - invisible",
points=[
common_types.OrderedPair(0, 0),
common_types.OrderedPair(-13.3136, 41.0321),
common_types.OrderedPair(21.3891, 16.8057),
],
visible=False,
),
),
(
"""
<object id="12" x="124.1878" y="90.1398" name="polyline - rotated" rotation="110">
<polyline points="0,0 -13.3136,41.0321 21.3891,16.8057"/>
</object>
""",
Polyline(
id=12,
coordinates=common_types.OrderedPair(124.1878, 90.1398),
name="polyline - rotated",
points=[
common_types.OrderedPair(0, 0),
common_types.OrderedPair(-13.3136, 41.0321),
common_types.OrderedPair(21.3891, 16.8057),
],
rotation=110,
),
),
]
TEXTS = [
(
"""
<object id="19" x="93.2987" y="81.7106" name="text" width="92.375" height="19">
<text>Hello World</text>
</object>
""",
Text(
id=19,
name="text",
text="Hello World",
size=common_types.Size(92.375, 19),
coordinates=common_types.OrderedPair(93.2987, 81.7106),
),
),
(
"""
<object id="19" x="93.2987" y="81.7106" name="text - wrap" width="92.375" height="19">
<text wrap="1">Hello World</text>
</object>
""",
Text(
id=19,
name="text - wrap",
text="Hello World",
wrap=True,
size=common_types.Size(92.375, 19),
coordinates=common_types.OrderedPair(93.2987, 81.7106),
),
),
(
"""
<object id="19" x="93.2987" y="81.7106" name="text - rotated" width="92.375" height="19" rotation="110">
<text>Hello World</text>
</object>
""",
Text(
id=19,
name="text - rotated",
text="Hello World",
rotation=110,
size=common_types.Size(92.375, 19),
coordinates=common_types.OrderedPair(93.2987, 81.7106),
),
),
(
"""
<object id="19" x="93.2987" y="81.7106" name="text - different font" width="92.375" height="19" rotation="110">
<text fontfamily="DejaVu Sans" pixelsize="19">Hello World</text>
</object>
""",
Text(
id=19,
name="text - different font",
text="Hello World",
font_size=19,
font_family="DejaVu Sans",
rotation=110,
size=common_types.Size(92.375, 19),
coordinates=common_types.OrderedPair(93.2987, 81.7106),
),
),
(
"""
<object id="19" x="93.2987" y="81.7106" name="text - right bottom align" width="92.375" height="19">
<text halign="right" valign="bottom">Hello World</text>
</object>
""",
Text(
id=19,
name="text - right bottom align",
text="Hello World",
horizontal_align="right",
vertical_align="bottom",
size=common_types.Size(92.375, 19),
coordinates=common_types.OrderedPair(93.2987, 81.7106),
),
),
(
"""
<object id="19" x="93.2987" y="81.7106" name="text - center center align" width="92.375" height="19">
<text halign="center" valign="center">Hello World</text>
</object>
""",
Text(
id=19,
name="text - center center align",
text="Hello World",
horizontal_align="center",
vertical_align="center",
size=common_types.Size(92.375, 19),
coordinates=common_types.OrderedPair(93.2987, 81.7106),
),
),
(
"""
<object id="19" x="93.2987" y="81.7106" name="text - justified" width="92.375" height="19">
<text halign="justify">Hello World</text>
</object>
""",
Text(
id=19,
name="text - justified",
text="Hello World",
horizontal_align="justify",
size=common_types.Size(92.375, 19),
coordinates=common_types.OrderedPair(93.2987, 81.7106),
),
),
(
"""
<object id="19" x="93.2987" y="81.7106" name="text - colored" width="92.375" height="19">
<text color="#aa0000">Hello World</text>
</object>
""",
Text(
id=19,
name="text - colored",
text="Hello World",
color=common_types.Color(170, 0, 0, 255),
size=common_types.Size(92.375, 19),
coordinates=common_types.OrderedPair(93.2987, 81.7106),
),
),
(
"""
<object id="19" x="93.2987" y="81.7106" name="text - font options" width="92.375" height="19">
<text bold="1" italic="1" kerning="1" strikeout="1" underline="1" wrap="1">Hello World</text>
</object>
""",
Text(
id=19,
name="text - font options",
text="Hello World",
size=common_types.Size(92.375, 19),
bold=True,
italic=True,
kerning=True,
strike_out=True,
underline=True,
wrap=True,
coordinates=common_types.OrderedPair(93.2987, 81.7106),
),
),
]
TILES = [
(
"""
<object id="13" x="111.8981" y="48.3019" width="32" height="32" name="tile" gid="79"/>
""",
Tile(
id=13,
size=common_types.Size(32, 32),
name="tile",
coordinates=common_types.OrderedPair(111.8981, 48.3019),
gid=79,
),
),
(
"""
<object type="tile" id="13" x="111.8981" y="48.3019" width="32" height="32" name="tile - invisible" gid="79" visible="0"/>
""",
Tile(
id=13,
size=common_types.Size(32, 32),
name="tile - invisible",
type="tile",
coordinates=common_types.OrderedPair(111.8981, 48.3019),
gid=79,
visible=False,
),
),
(
"""
<object id="13" x="111.8981" y="48.3019" width="32" height="32" name="tile - rotated" gid="79" rotation="110"/>
""",
Tile(
id=13,
size=common_types.Size(32, 32),
name="tile - rotated",
coordinates=common_types.OrderedPair(111.8981, 48.3019),
gid=79,
rotation=110,
),
),
]
OBJECTS = ELLIPSES + RECTANGLES + POINTS + POLYGONS + POLYLINES + TEXTS + TILES
@pytest.mark.parametrize("raw_object_tmx,expected", OBJECTS)
def test_parse_layer(raw_object_tmx, expected):
raw_object = etree.fromstring(raw_object_tmx)
result = parse(raw_object)
assert result == expected

View File

@@ -2,11 +2,14 @@
import importlib.util
import json
import os
import xml.etree.ElementTree as etree
from pathlib import Path
import pytest
from pytiled_parser import tileset
from pytiled_parser.common_types import OrderedPair, Size
from pytiled_parser.parsers.json.tileset import parse as parse_json
from pytiled_parser.parsers.tmx.tileset import parse as parse_tmx
TESTS_DIR = Path(os.path.dirname(os.path.abspath(__file__)))
TEST_DATA = TESTS_DIR / "test_data"
@@ -26,8 +29,26 @@ ALL_TILESET_DIRS = [
]
def fix_object(my_object):
my_object.coordinates = OrderedPair(
round(my_object.coordinates[0], 4), round(my_object.coordinates[1], 4)
)
my_object.size = Size(round(my_object.size[0], 4), round(my_object.size[1], 4))
def fix_tileset(tileset):
tileset.version = None
tileset.tiled_version = None
if tileset.tiles:
for tile in tileset.tiles.values():
if tile.objects:
for my_object in tile.objects.tiled_objects:
fix_object(my_object)
@pytest.mark.parametrize("parser_type", ["json", "tmx"])
@pytest.mark.parametrize("tileset_dir", ALL_TILESET_DIRS)
def test_tilesets_integration(tileset_dir):
def test_tilesets_integration(parser_type, tileset_dir):
# it's a PITA to import like this, don't do it
# https://stackoverflow.com/a/67692/1342874
spec = importlib.util.spec_from_file_location(
@@ -36,9 +57,16 @@ def test_tilesets_integration(tileset_dir):
expected = importlib.util.module_from_spec(spec)
spec.loader.exec_module(expected)
raw_tileset_path = tileset_dir / "tileset.json"
if parser_type == "json":
raw_tileset_path = tileset_dir / "tileset.json"
with open(raw_tileset_path) as raw_tileset:
tileset_ = parse_json(json.loads(raw_tileset.read()), 1)
elif parser_type == "tmx":
raw_tileset_path = tileset_dir / "tileset.tsx"
with open(raw_tileset_path) as raw_tileset:
tileset_ = parse_tmx(etree.parse(raw_tileset).getroot(), 1)
with open(raw_tileset_path) as raw_tileset:
tileset_ = tileset.cast(json.loads(raw_tileset.read()), 1)
fix_tileset(tileset_)
fix_tileset(expected.EXPECTED)
assert tileset_ == expected.EXPECTED