Merge pull request #1 from Beefy-Swain/master

update
This commit is contained in:
Paul V Craven
2019-05-27 12:19:39 -05:00
committed by GitHub
13 changed files with 1224 additions and 804 deletions

View File

@@ -1,4 +1,4 @@
from . import utilities from . import utilities
from . import objects from . import objects
from .parser import parse_tile_map from .xml_parser import parse_tile_map

View File

@@ -11,30 +11,11 @@ from pathlib import Path
import xml.etree.ElementTree as etree import xml.etree.ElementTree as etree
from typing import * # pylint: disable=W0401 from typing import NamedTuple, Union, Optional, List, Dict
class EncodingError(Exception):
"""
Tmx layer encoding is of an unknown type.
"""
class TileNotFoundError(Exception):
"""
Tile not found in tileset.
"""
class ImageNotFoundError(Exception):
"""
Image not found.
"""
class Color(NamedTuple): class Color(NamedTuple):
""" """Color object.
Color object.
Attributes: Attributes:
:red (int): Red, between 1 and 255. :red (int): Red, between 1 and 255.
@@ -42,6 +23,7 @@ class Color(NamedTuple):
:blue (int): Blue, between 1 and 255. :blue (int): Blue, between 1 and 255.
:alpha (int): Alpha, between 1 and 255. :alpha (int): Alpha, between 1 and 255.
""" """
red: int red: int
green: int green: int
blue: int blue: int
@@ -49,17 +31,29 @@ class Color(NamedTuple):
class OrderedPair(NamedTuple): class OrderedPair(NamedTuple):
""" """OrderedPair NamedTuple.
OrderedPair NamedTuple.
Attributes: Attributes:
:x (Union[int, float]): X coordinate. x (Union[int, float]): X coordinate.
:y (Union[int, float]): Y coordinate. y (Union[int, float]): Y coordinate.
""" """
x: Union[int, float] x: Union[int, float]
y: Union[int, float] y: Union[int, float]
class Size(NamedTuple):
"""Size NamedTuple.
Attributes:
width (Union[int, float]): The width of the object.
size (Union[int, float]): The height of the object.
"""
width: Union[int, float]
height: Union[int, float]
class Template: class Template:
""" """
FIXME TODO FIXME TODO
@@ -80,13 +74,15 @@ class Chunk:
:layer_data (List[List(int)]): The global tile IDs in chunky :layer_data (List[List(int)]): The global tile IDs in chunky
according to row. according to row.
""" """
location: OrderedPair location: OrderedPair
width: int width: int
height: int height: int
chunk_data: List[List[int]] chunk_data: List[List[int]]
class Image(NamedTuple): @dataclasses.dataclass
class Image:
""" """
Image object. Image object.
@@ -101,9 +97,10 @@ class Image(NamedTuple):
(optional, used for tile index correction when the image changes). (optional, used for tile index correction when the image changes).
:height (Optional[str]): The image height in pixels (optional). :height (Optional[str]): The image height in pixels (optional).
""" """
source: str source: str
size: OrderedPair size: Optional[Size] = None
trans: Optional[Color] trans: Optional[Color] = None
Properties = Dict[str, Union[int, float, Color, Path, str]] Properties = Dict[str, Union[int, float, Color, Path, str]]
@@ -117,6 +114,7 @@ class Grid(NamedTuple):
determines how tile overlays for terrain and collision information determines how tile overlays for terrain and collision information
are rendered. are rendered.
""" """
orientation: str orientation: str
width: int width: int
height: int height: int
@@ -131,6 +129,7 @@ class Terrain(NamedTuple):
:tile (int): The local tile-id of the tile that represents the :tile (int): The local tile-id of the tile that represents the
terrain visually. terrain visually.
""" """
name: str name: str
tile: int tile: int
@@ -147,6 +146,7 @@ class Frame(NamedTuple):
:duration (int): How long in milliseconds this frame should be :duration (int): How long in milliseconds this frame should be
displayed before advancing to the next frame. displayed before advancing to the next frame.
""" """
tile_id: int tile_id: int
duration: int duration: int
@@ -165,6 +165,7 @@ class TileTerrain:
:bottom_left (Optional[int]): Bottom left terrain type. :bottom_left (Optional[int]): Bottom left terrain type.
:bottom_right (Optional[int]): Bottom right terrain type. :bottom_right (Optional[int]): Bottom right terrain type.
""" """
top_left: Optional[int] = None top_left: Optional[int] = None
top_right: Optional[int] = None top_right: Optional[int] = None
bottom_left: Optional[int] = None bottom_left: Optional[int] = None
@@ -172,44 +173,35 @@ class TileTerrain:
@dataclasses.dataclass @dataclasses.dataclass
class _LayerTypeBase: class Layer:
id: int # pylint: disable=C0103 """Class that all layers inherret from.
name: str
@dataclasses.dataclass
class _LayerTypeDefaults:
offset: OrderedPair = OrderedPair(0, 0)
opacity: int = 0xFF
properties: Optional[Properties] = None
@dataclasses.dataclass
class LayerType(_LayerTypeDefaults, _LayerTypeBase):
"""
Class that all layer classes inherit from.
Not to be directly used.
Args: Args:
:layer_element (etree.Element): Element to be parsed into a id: Unique ID of the layer. Each layer that added to a map gets a
LayerType object. unique id. Even if a layer is deleted, no layer ever gets the same
ID.
Attributes: name: The name of the layer object.
:id (int): Unique ID of the layer. Each layer that added to a map tiled_objects: List of tiled_objects in the layer.
gets a unique id. Even if a layer is deleted, no layer ever gets offset: Rendering offset of the layer object in pixels.
the same ID. opacity: Decimal value between 0 and 1 to determine opacity. 1 is
:name (Optional[str):] The name of the layer object. completely opaque, 0 is completely transparent.
:offset (OrderedPair): Rendering offset of the layer object in properties: Properties for the layer.
pixels. (default: (0, 0). color: The color used to display the objects in this group.
:opacity (int): Value between 0 and 255 to determine opacity. NOTE: FIXME: editor only?
this value is converted from a float provided by Tiled, so some draworder: Whether the objects are drawn according to the order of the
precision is lost. object elements in the object group element ('manual'), or sorted
:properties (Optional[Properties]): Properties object for layer by their y-coordinate ('topdown'). Defaults to 'topdown'. See:
object. https://doc.mapeditor.org/en/stable/manual/objects/#changing-stacking-order
for more info.
""" """
id: int
name: str
offset: Optional[OrderedPair]
opacity: Optional[float]
properties: Optional[Properties]
LayerData = Union[List[List[int]], List[Chunk]] LayerData = Union[List[List[int]], List[Chunk]]
""" """
@@ -221,38 +213,34 @@ Either a 2 dimensional array of integers representing the global tile IDs
@dataclasses.dataclass @dataclasses.dataclass
class _LayerBase: class TileLayer(Layer):
size: OrderedPair """Tile map layer containing tiles.
See: https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#layer
Args:
size: The width of the layer in tiles. The same as the map width
unless map is infitite.
data: Either an 2 dimensional array of integers representing the
global tile IDs for the map layer, or a list of chunks for an
infinite map.
"""
size: Size
data: LayerData data: LayerData
@dataclasses.dataclass @dataclasses.dataclass
class Layer(LayerType, _LayerBase): class _TiledObjectBase:
"""
Map layer object.
See: https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#layer
Attributes:
:size (OrderedPair): The width of the layer in tiles. Always the same
as the map width for not infitite maps.
:data (LayerData): Either an 2 dimensional array of integers
representing the global tile IDs for the map layer, or a list of
chunks for an infinite map.
"""
@dataclasses.dataclass
class _ObjectBase:
id: int id: int
location: OrderedPair location: OrderedPair
@dataclasses.dataclass @dataclasses.dataclass
class _ObjectDefaults: class _TiledObjectDefaults:
size: OrderedPair = OrderedPair(0, 0) size: Size = Size(0, 0)
rotation: int = 0 rotation: int = 0
opacity: int = 0xFF opacity: float = 1
name: Optional[str] = None name: Optional[str] = None
type: Optional[str] = None type: Optional[str] = None
@@ -262,33 +250,33 @@ class _ObjectDefaults:
@dataclasses.dataclass @dataclasses.dataclass
class Object(_ObjectDefaults, _ObjectBase): class TiledObject(_TiledObjectDefaults, _TiledObjectBase):
""" """
ObjectGroup Object. TiledObject object.
See: \ See:
https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#object https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#object
Args: Args:
:id (int): Unique ID of the object. Each object that is placed on a :id (int): Unique ID of the object. Each object that is placed on a
map gets a unique id. Even if an object was deleted, no object map gets a unique id. Even if an object was deleted, no object
gets the same ID. gets the same ID.
:location (OrderedPair): The location of the object in pixels. :location (OrderedPair): The location of the object in pixels.
:size (OrderedPair): The width of the object in pixels :size (Size): The width of the object in pixels
(default: (0, 0)). (default: (0, 0)).
:rotation (int): The rotation of the object in degrees clockwise :rotation (int): The rotation of the object in degrees clockwise
(default: 0). (default: 0).
:opacity (int): The opacity of the object. (default: 255) :opacity (int): The opacity of the object. (default: 255)
:name (Optional[str]): The name of the object. :name (Optional[str]): The name of the object.
:type (Optional[str]): The type of the object. :type (Optional[str]): The type of the object.
:properties (Properties): The properties of the Object. :properties (Properties): The properties of the TiledObject.
:template Optional[Template]: A reference to a Template object :template Optional[Template]: A reference to a Template object
FIXME FIXME
""" """
@dataclasses.dataclass @dataclasses.dataclass
class RectangleObject(Object): class RectangleObject(TiledObject):
""" """
Rectangle shape defined by a point, width, and height. Rectangle shape defined by a point, width, and height.
@@ -299,7 +287,7 @@ class RectangleObject(Object):
@dataclasses.dataclass @dataclasses.dataclass
class ElipseObject(Object): class ElipseObject(TiledObject):
""" """
Elipse shape defined by a point, width, and height. Elipse shape defined by a point, width, and height.
@@ -308,7 +296,7 @@ class ElipseObject(Object):
@dataclasses.dataclass @dataclasses.dataclass
class PointObject(Object): class PointObject(TiledObject):
""" """
Point defined by a point (x,y). Point defined by a point (x,y).
@@ -317,12 +305,12 @@ class PointObject(Object):
@dataclasses.dataclass @dataclasses.dataclass
class _TileObjectBase(_ObjectBase): class _TileImageObjectBase(_TiledObjectBase):
gid: int gid: int
@dataclasses.dataclass @dataclasses.dataclass
class TileObject(Object, _TileObjectBase): class TileImageObject(TiledObject, _TileImageObjectBase):
""" """
Polygon shape defined by a set of connections between points. Polygon shape defined by a set of connections between points.
@@ -334,12 +322,12 @@ class TileObject(Object, _TileObjectBase):
@dataclasses.dataclass @dataclasses.dataclass
class _PointsObjectBase(_ObjectBase): class _PointsObjectBase(_TiledObjectBase):
points: List[OrderedPair] points: List[OrderedPair]
@dataclasses.dataclass @dataclasses.dataclass
class PolygonObject(Object, _PointsObjectBase): class PolygonObject(TiledObject, _PointsObjectBase):
""" """
Polygon shape defined by a set of connections between points. Polygon shape defined by a set of connections between points.
@@ -351,12 +339,12 @@ class PolygonObject(Object, _PointsObjectBase):
@dataclasses.dataclass @dataclasses.dataclass
class PolylineObject(Object, _PointsObjectBase): class PolylineObject(TiledObject, _PointsObjectBase):
""" """
Polyline defined by a set of connections between points. Polyline defined by a set of connections between points.
See: \ See:
https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#polyline https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#polyline
Attributes: Attributes:
:points (List[Tuple[int, int]]): List of coordinates relative to \ :points (List[Tuple[int, int]]): List of coordinates relative to \
@@ -365,27 +353,27 @@ https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#polyline
@dataclasses.dataclass @dataclasses.dataclass
class _TextObjectBase(_ObjectBase): class _TextObjectBase(_TiledObjectBase):
text: str text: str
@dataclasses.dataclass @dataclasses.dataclass
class _TextObjectDefaults(_ObjectDefaults): class _TextObjectDefaults(_TiledObjectDefaults):
font_family: str = 'sans-serif' font_family: str = "sans-serif"
font_size: int = 16 font_size: int = 16
wrap: bool = False wrap: bool = False
color: Color = Color(0xFF, 0, 0, 0) color: str = "#000000"
bold: bool = False bold: bool = False
italic: bool = False italic: bool = False
underline: bool = False underline: bool = False
strike_out: bool = False strike_out: bool = False
kerning: bool = False kerning: bool = False
horizontal_align: str = 'left' horizontal_align: str = "left"
vertical_align: str = 'top' vertical_align: str = "top"
@dataclasses.dataclass @dataclasses.dataclass
class TextObject(Object, _TextObjectDefaults, _TextObjectBase): class TextObject(TiledObject, _TextObjectDefaults, _TextObjectBase):
""" """
Text object with associated settings. Text object with associated settings.
@@ -410,45 +398,36 @@ class TextObject(Object, _TextObjectDefaults, _TextObjectBase):
@dataclasses.dataclass @dataclasses.dataclass
class _ObjectGroupBase(_LayerTypeBase): class ObjectLayer(Layer):
objects: List[Object]
@dataclasses.dataclass
class _ObjectGroupDefaults(_LayerTypeDefaults):
color: Optional[Color] = None
draw_order: Optional[str] = 'topdown'
@dataclasses.dataclass
class ObjectGroup(LayerType, _ObjectGroupDefaults, _ObjectGroupBase):
""" """
Object Group Object. TiledObject Group Object.
The object group is in fact a map layer, and is hence called \ The object group is in fact a map layer, and is hence called \
“object layer” in Tiled. “object layer” in Tiled.
See: \ See:
https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#objectgroup https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#objectgroup
Attributes: Args:
:color (Optional[Color]): The color used to display the objects tiled_objects: List of tiled_objects in the layer.
in this group. FIXME: editor only? offset: Rendering offset of the layer object in pixels.
:draworder (str): Whether the objects are drawn according to the color: The color used to display the objects in this group.
order of the object elements in the object group element FIXME: editor only?
('manual'), or sorted by their y-coordinate ('topdown'). Defaults draworder: Whether the objects are drawn according to the order of the
to 'topdown'. See: object elements in the object group element ('manual'), or sorted
by their y-coordinate ('topdown'). Defaults to 'topdown'. See:
https://doc.mapeditor.org/en/stable/manual/objects/#changing-stacking-order https://doc.mapeditor.org/en/stable/manual/objects/#changing-stacking-order
for more info. for more info.
:objects (Dict[int, Object]): Dict Object objects by Object.id.
""" """
tiled_objects: List[TiledObject]
class _LayerGroupBase(_LayerTypeBase): color: Optional[str] = None
layers: Optional[List[LayerType]] draw_order: Optional[str] = "topdown"
class LayerGroup(LayerType): @dataclasses.dataclass
class LayerGroup(Layer):
""" """
Layer Group. Layer Group.
@@ -463,6 +442,14 @@ class LayerGroup(LayerType):
""" """
layers: Optional[List[Union["LayerGroup", Layer, ObjectLayer]]]
@dataclasses.dataclass
class Hitbox:
"""Group of hitboxes for
"""
@dataclasses.dataclass @dataclasses.dataclass
class Tile: class Tile:
@@ -477,12 +464,13 @@ class Tile:
:animation (List[Frame]): Each tile can have exactly one animation :animation (List[Frame]): Each tile can have exactly one animation
associated with it. associated with it.
""" """
id: int id: int
type: Optional[str] type: Optional[str]
terrain: Optional[TileTerrain] terrain: Optional[TileTerrain]
animation: Optional[List[Frame]] animation: Optional[List[Frame]]
image: Optional[Image] image: Optional[Image]
hit_box: Optional[List[Object]] hitboxes: Optional[List[TiledObject]]
@dataclasses.dataclass @dataclasses.dataclass
@@ -492,7 +480,7 @@ class TileSet:
Args: Args:
:name (str): The name of this tileset. :name (str): The name of this tileset.
:max_tile_size (OrderedPair): The maximum size of a tile in this :max_tile_size (Size): The maximum size of a tile in this
tile set in pixels. tile set in pixels.
:spacing (int): The spacing in pixels between the tiles in this :spacing (int): The spacing in pixels between the tiles in this
tileset (applies to the tileset image). tileset (applies to the tileset image).
@@ -515,8 +503,9 @@ class TileSet:
file. file.
:tiles (Optional[Dict[int, Tile]]): Dict of Tile objects by Tile.id. :tiles (Optional[Dict[int, Tile]]): Dict of Tile objects by Tile.id.
""" """
name: str name: str
max_tile_size: OrderedPair max_tile_size: Size
spacing: Optional[int] spacing: Optional[int]
margin: Optional[int] margin: Optional[int]
tile_count: Optional[int] tile_count: Optional[int]
@@ -529,6 +518,9 @@ class TileSet:
tiles: Optional[Dict[int, Tile]] tiles: Optional[Dict[int, Tile]]
TileSetDict = Dict[int, TileSet]
@dataclasses.dataclass @dataclasses.dataclass
class TileMap: class TileMap:
""" """
@@ -548,8 +540,8 @@ class TileMap:
rendered. Valid values are right-down, right-up, left-down and rendered. Valid values are right-down, right-up, left-down and
left-up. In all cases, the map is drawn row-by-row. (only left-up. In all cases, the map is drawn row-by-row. (only
supported for orthogonal maps at the moment) supported for orthogonal maps at the moment)
:map_size (OrderedPair): The map width in tiles. :map_size (Size): The map width in tiles.
:tile_size (OrderedPair): The width of a tile. :tile_size (Size): The width of a tile.
:infinite (bool): If the map is infinite or not. :infinite (bool): If the map is infinite or not.
:hexsidelength (int): Only for hexagonal maps. Determines the width or :hexsidelength (int): Only for hexagonal maps. Determines the width or
height (depending on the staggered axis) of the tiles edge, in height (depending on the staggered axis) of the tiles edge, in
@@ -563,34 +555,35 @@ class TileMap:
:nextlayerid (int): Stores the next available ID for new layers. :nextlayerid (int): Stores the next available ID for new layers.
:nextobjectid (int): Stores the next available ID for new objects. :nextobjectid (int): Stores the next available ID for new objects.
:tile_sets (dict[str, TileSet]): Dict of tile sets used :tile_sets (dict[str, TileSet]): Dict of tile sets used
in this map. Key is the source for external tile sets or the name in this map. Key is the first GID for the tile set. The value
for embedded ones. The value is a TileSet object. is a TileSet object.
:layers List[LayerType]: List of layer objects by draw order. :layers List[LayerType]: List of layer objects by draw order.
""" """
parent_dir: Path parent_dir: Path
version: str version: str
tiled_version: str tiled_version: str
orientation: str orientation: str
render_order: str render_order: str
map_size: OrderedPair map_size: Size
tile_size: OrderedPair tile_size: Size
infinite: bool infinite: bool
next_layer_id: int next_layer_id: int
next_object_id: int next_object_id: int
tile_sets: Dict[int, TileSet] tile_sets: TileSetDict
layers: List[LayerType] layers: List[Layer]
hex_side_length: Optional[int] = None hex_side_length: Optional[int] = None
stagger_axis: Optional[int] = None stagger_axis: Optional[int] = None
stagger_index: Optional[int] = None stagger_index: Optional[int] = None
background_color: Optional[Color] = None background_color: Optional[str] = None
properties: Optional[Properties] = None properties: Optional[Properties] = None
''' """
[22:16] <__m4ch1n3__> i would "[i for i in int_list if i < littler_then_value]" [22:16] <__m4ch1n3__> i would "[i for i in int_list if i < littler_then_value]"
[22:16] <__m4ch1n3__> it returns a list of integers below "littler_then_value" [22:16] <__m4ch1n3__> it returns a list of integers below "littler_then_value"
[22:17] <__m4ch1n3__> !py3 [i for i in [1,2,3,4,1,2,3,4] if i < 3] [22:17] <__m4ch1n3__> !py3 [i for i in [1,2,3,4,1,2,3,4] if i < 3]
@@ -606,6 +599,4 @@ class TileMap:
[22:23] <codebot> __m4ch1n3__: 100 [22:23] <codebot> __m4ch1n3__: 100
[22:23] == markb1 [~mbiggers@45.36.35.206] has quit [Ping timeout: 245 seconds] [22:23] == markb1 [~mbiggers@45.36.35.206] has quit [Ping timeout: 245 seconds]
[22:23] <__m4ch1n3__> !py3 max(i for i in [1, 10, 100] if i < 242) [22:23] <__m4ch1n3__> !py3 max(i for i in [1, 10, 100] if i < 242)
''' """
#buffer

View File

@@ -1,581 +0,0 @@
import functools
import re
import base64
import zlib
from pathlib import Path
from typing import *
import pytiled_parser.objects as objects
import pytiled_parser.utilities as utilities
import xml.etree.ElementTree as etree
def _decode_base64_data(data_text, compression, layer_width):
tile_grid: List[List[int]] = [[]]
unencoded_data = base64.b64decode(data_text)
if compression == "zlib":
unzipped_data = zlib.decompress(unencoded_data)
elif compression == "gzip":
unzipped_data = gzip.decompress(unencoded_data)
elif compression is None:
unzipped_data = unencoded_data
else:
raise ValueError(f"Unsupported compression type '{compression}'.")
# Turn bytes into 4-byte integers
byte_count = 0
int_count = 0
int_value = 0
row_count = 0
for byte in unzipped_data:
int_value += byte << (byte_count * 8)
byte_count += 1
if byte_count % 4 == 0:
byte_count = 0
int_count += 1
tile_grid[row_count].append(int_value)
int_value = 0
if int_count % layer_width == 0:
row_count += 1
tile_grid.append([])
tile_grid.pop()
return tile_grid
def _decode_csv_layer(data_text):
"""
Decodes csv encoded layer data.
Credit:
"""
tile_grid = []
lines = data_text.split("\n")
# remove erronious empty lists due to a newline being on both ends of text
lines = lines[1:]
lines = lines[:-1]
for line in lines:
line_list = line.split(",")
while '' in line_list:
line_list.remove('')
line_list_int = [int(item) for item in line_list]
tile_grid.append(line_list_int)
return tile_grid
def _decode_data(element: etree.Element, layer_width: int, encoding: str,
compression: Optional[str]) -> List[List[int]]:
"""
Decodes data or chunk data.
Args:
:element (Element): Element to have text decoded.
:layer_width (int): Number of tiles per column in this layer. Used
for determining when to cut off a row when decoding base64
encoding layers.
:encoding (str): Encoding format of the layer data.
:compression (str): Compression format of the layer data.
"""
# etree.Element.text comes with an appended and a prepended '\n'
supported_encodings = ['base64', 'csv']
if encoding not in supported_encodings:
raise ValueError('{encoding} is not a valid encoding')
supported_compression = [None, 'gzip', 'zlib']
if compression is not None:
if encoding != 'base64':
raise ValueError('{encoding} does not support compression')
if compression not in supported_compression:
raise ValueError('{compression} is not a valid compression type')
try:
data_text = element.text # type: ignore
except AttributeError:
raise AttributeError('{element} lacks layer data.')
if encoding == 'csv':
return _decode_csv_layer(data_text)
return _decode_base64_data(data_text, compression, layer_width)
def _parse_data(element: etree.Element,
layer_width: int) -> objects.LayerData:
"""
Parses layer data.
Will parse CSV, base64, gzip-base64, or zlip-base64 encoded data.
Args:
:element (Element): Data element to parse.
:width (int): Layer width. Used for base64 decoding.
Returns:
:LayerData: Data object containing layer data or chunks of data.
"""
encoding = element.attrib['encoding']
compression = None
try:
compression = element.attrib['compression']
except KeyError:
pass
chunk_elements = element.findall('./chunk')
if chunk_elements:
chunks: List[objects.Chunk] = []
for chunk_element in chunk_elements:
x = int(chunk_element.attrib['x'])
y = int(chunk_element.attrib['y'])
location = objects.OrderedPair(x, y)
width = int(chunk_element.attrib['width'])
height = int(chunk_element.attrib['height'])
layer_data = _decode_data(chunk_element, layer_width, encoding,
compression)
chunks.append(objects.Chunk(location, width, height, layer_data))
return chunks
return _decode_data(element, layer_width, encoding, compression)
def _parse_layer(element: etree.Element,
layer_type: objects.LayerType) -> objects.Layer:
"""
Parse layer element given.
"""
width = int(element.attrib['width'])
height = int(element.attrib['height'])
size = objects.OrderedPair(width, height)
data_element = element.find('./data')
if data_element is not None:
data: objects.LayerData = _parse_data(data_element, width)
else:
raise ValueError('{element} has no child data element.')
return objects.Layer(size, data, **layer_type.__dict__)
def _parse_layer_type(layer_element: etree.Element) -> objects.LayerType:
"""
Parse layer type element given.
"""
id = int(layer_element.attrib['id'])
name = layer_element.attrib['name']
layer_type_object = objects.LayerType(id, name)
try:
offset_x = float(layer_element.attrib['offsetx'])
except KeyError:
offset_x = 0
try:
offset_y = float(layer_element.attrib['offsety'])
except KeyError:
offset_y = 0
offset = objects.OrderedPair(offset_x, offset_y)
try:
layer_type_object.opacity = round(
float(layer_element.attrib['opacity']) * 255)
except KeyError:
pass
properties_element = layer_element.find('./properties')
if properties_element is not None:
layer_type_object.properties = _parse_properties_element(
properties_element)
if layer_element.tag == 'layer':
return _parse_layer(layer_element, layer_type_object)
elif layer_element.tag == 'objectgroup':
return _parse_object_group(layer_element, layer_type_object)
# else:
# return _parse_layer_group(layer_element, layer_type_object)
def _parse_object_group(element: etree.Element,
layer_type: objects.LayerType) -> objects.ObjectGroup:
"""
Parse object group element given.
"""
object_elements = element.findall('./object')
tile_objects: List[objects.Object] = []
for object_element in object_elements:
id = int(object_element.attrib['id'])
location_x = float(object_element.attrib['x'])
location_y = float(object_element.attrib['y'])
location = objects.OrderedPair(location_x, location_y)
object = objects.Object(id, location)
try:
width = float(object_element.attrib['width'])
except KeyError:
width = 0
try:
height = float(object_element.attrib['height'])
except KeyError:
height = 0
object.size = objects.OrderedPair(width, height)
try:
object.opacity = round(
float(object_element.attrib['opacity']) * 255)
except KeyError:
pass
try:
object.rotation = int(object_element.attrib['rotation'])
except KeyError:
pass
try:
object.name = object_element.attrib['name']
except KeyError:
pass
properties_element = object_element.find('./properties')
if properties_element is not None:
object.properties = _parse_properties_element(properties_element)
tile_objects.append(object)
object_group = objects.ObjectGroup(tile_objects, **layer_type.__dict__)
try:
color = utilities.parse_color(element.attrib['color'])
except KeyError:
pass
try:
draw_order = element.attrib['draworder']
except KeyError:
pass
return object_group
@functools.lru_cache()
def _parse_external_tile_set(parent_dir: Path, tile_set_element: etree.Element
) -> objects.TileSet:
"""
Parses an external tile set.
Caches the results to speed up subsequent instances.
"""
source = Path(tile_set_element.attrib['source'])
tile_set_tree = etree.parse(str(parent_dir / Path(source))).getroot()
return _parse_tile_set(tile_set_tree)
def _parse_tiles(tile_element_list: List[etree.Element]
) -> Dict[int, objects.Tile]:
tiles: Dict[int, objects.Tile] = {}
for tile_element in tile_element_list:
# id is not optional
id = int(tile_element.attrib['id'])
# optional attributes
type = None
try:
type = tile_element.attrib['type']
except KeyError:
pass
tile_terrain = None
try:
tile_terrain_attrib = tile_element.attrib['terrain']
except KeyError:
pass
else:
# an attempt to explain how terrains are handled is below.
# 'terrain' attribute is a comma seperated list of 4 values,
# each is either an integer or blank
# convert to list of values
terrain_list_attrib = re.split(',', tile_terrain_attrib)
# terrain_list is list of indexes of Tileset.terrain_types
terrain_list: List[Optional[int]] = []
# each index in terrain_list_attrib reffers to a corner
for corner in terrain_list_attrib:
if corner == '':
terrain_list.append(None)
else:
terrain_list.append(int(corner))
tile_terrain = objects.TileTerrain(*terrain_list)
# tile element optional sub-elements
animation: Optional[List[objects.Frame]] = None
tile_animation_element = tile_element.find('./animation')
if tile_animation_element:
animation = []
frames = tile_animation_element.findall('./frame')
for frame in frames:
# tileid reffers to the Tile.id of the animation frame
tile_id = int(frame.attrib['tileid'])
# duration is in MS. Should perhaps be converted to seconds.
# FIXME: make decision
duration = int(frame.attrib['duration'])
animation.append(objects.Frame(tile_id, duration))
# if this is None, then the Tile is part of a spritesheet
tile_image = None
tile_image_element = tile_element.find('./image')
if tile_image_element is not None:
tile_image = _parse_image_element(tile_image_element)
object_group = None
tile_object_group_element = tile_element.find('./objectgroup')
if tile_object_group_element:
### FIXME: why did they do this :(
pass
tiles[id] = objects.Tile(id, type, tile_terrain, animation,
tile_image, object_group)
return tiles
def _parse_image_element(image_element: etree.Element) -> objects.Image:
"""
Parse image element given.
Returns:
:Color: Color in Arcade's preffered format.
"""
source = image_element.attrib['source']
trans = None
try:
trans = utilities.parse_color(image_element.attrib['trans'])
except KeyError:
pass
width = int(image_element.attrib['width'])
height = int(image_element.attrib['height'])
size = objects.OrderedPair(width, height)
return objects.Image(source, size, trans)
def _parse_properties_element(properties_element: etree.Element
) -> objects.Properties:
"""
Adds Tiled property to Properties dict.
Args:
:name (str): Name of property.
:property_type (str): Type of property. Can be string, int, float,
bool, color or file. Defaults to string.
:value (str): The value of the property.
Returns:
:Properties: Properties Dict object.
"""
properties: objects.Properties = {}
for property_element in properties_element.findall('./property'):
name = property_element.attrib['name']
try:
property_type = property_element.attrib['type']
except KeyError:
# strings do not have an attribute in property elements
property_type = 'string'
value = property_element.attrib['value']
property_types = ['string', 'int', 'float', 'bool', 'color', 'file']
assert property_type in property_types, (
f"Invalid type for property {name}")
if property_type == 'int':
properties[name] = int(value)
elif property_type == 'float':
properties[name] = float(value)
elif property_type == 'color':
properties[name] = utilities.parse_color(value)
elif property_type == 'file':
properties[name] = Path(value)
elif property_type == 'bool':
if value == 'true':
properties[name] = True
else:
properties[name] = False
else:
properties[name] = value
return properties
def _parse_tile_set(tile_set_element: etree.Element) -> objects.TileSet:
"""
Parses a tile set that is embedded into a TMX.
"""
# get all basic attributes
name = tile_set_element.attrib['name']
max_tile_width = int(tile_set_element.attrib['tilewidth'])
max_tile_height = int(tile_set_element.attrib['tileheight'])
max_tile_size = objects.OrderedPair(max_tile_width, max_tile_height)
spacing = None
try:
spacing = int(tile_set_element.attrib['spacing'])
except KeyError:
pass
margin = None
try:
margin = int(tile_set_element.attrib['margin'])
except KeyError:
pass
tile_count = None
try:
tile_count = int(tile_set_element.attrib['tilecount'])
except KeyError:
pass
columns = None
try:
columns = int(tile_set_element.attrib['columns'])
except KeyError:
pass
tile_offset = None
tileoffset_element = tile_set_element.find('./tileoffset')
if tileoffset_element is not None:
tile_offset_x = int(tileoffset_element.attrib['x'])
tile_offset_y = int(tileoffset_element.attrib['y'])
tile_offset = objects.OrderedPair(tile_offset_x, tile_offset_y)
grid = None
grid_element = tile_set_element.find('./grid')
if grid_element is not None:
grid_orientation = grid_element.attrib['orientation']
grid_width = int(grid_element.attrib['width'])
grid_height = int(grid_element.attrib['height'])
grid = objects.Grid(grid_orientation, grid_width, grid_height)
properties = None
properties_element = tile_set_element.find('./properties')
if properties_element is not None:
properties = _parse_properties_element(properties_element)
terrain_types: Optional[List[objects.Terrain]] = None
terrain_types_element = tile_set_element.find('./terraintypes')
if terrain_types_element is not None:
terrain_types = []
for terrain in terrain_types_element.findall('./terrain'):
name = terrain.attrib['name']
terrain_tile = int(terrain.attrib['tile'])
terrain_types.append(objects.Terrain(name, terrain_tile))
image = None
image_element = tile_set_element.find('./image')
if image_element is not None:
image = _parse_image_element(image_element)
tile_element_list = tile_set_element.findall('./tile')
tiles = _parse_tiles(tile_element_list)
return objects.TileSet(name, max_tile_size, spacing, margin, tile_count,
columns, tile_offset, grid, properties, image,
terrain_types, tiles)
def parse_tile_map(tmx_file: Union[str, Path]):
# setting up XML parsing
map_tree = etree.parse(str(tmx_file))
map_element = map_tree.getroot()
# positional arguments for TileMap
parent_dir = Path(tmx_file).parent
version = map_element.attrib['version']
tiled_version = map_element.attrib['tiledversion']
orientation = map_element.attrib['orientation']
render_order = map_element.attrib['renderorder']
map_width = int(map_element.attrib['width'])
map_height = int(map_element.attrib['height'])
map_size = objects.OrderedPair(map_width, map_height)
tile_width = int(map_element.attrib['tilewidth'])
tile_height = int(map_element.attrib['tileheight'])
tile_size = objects.OrderedPair(tile_width, tile_height)
infinite_attribute = map_element.attrib['infinite']
infinite = True if infinite_attribute == 'true' else False
next_layer_id = int(map_element.attrib['nextlayerid'])
next_object_id = int(map_element.attrib['nextobjectid'])
# parse all tilesets
tile_sets: Dict[int, objects.TileSet] = {}
tile_set_element_list = map_element.findall('./tileset')
for tile_set_element in tile_set_element_list:
# tiled docs are ambiguous about the 'firstgid' attribute
# current understanding is for the purposes of mapping the layer
# data to the tile set data, add the 'firstgid' value to each
# tile 'id'; this means that the 'firstgid' is specific to each,
# tile set as they pertain to the map, not tile set specific as
# the tiled docs can make it seem
# 'firstgid' is saved beside each TileMap
firstgid = int(tile_set_element.attrib['firstgid'])
try:
# check if is an external TSX
source = tile_set_element.attrib['source']
except KeyError:
# the tile set in embedded
name = tile_set_element.attrib['name']
tile_sets[firstgid] = _parse_tile_set(tile_set_element)
else:
# tile set is external
tile_sets[firstgid] = _parse_external_tile_set(
parent_dir, tile_set_element)
# parse all layers
layers: List[objects.LayerType] = []
layer_tags = ['layer', 'objectgroup', 'group']
for element in map_element.findall('./'):
if element.tag not in layer_tags:
# only layer_tags are layer elements
continue
layers.append(_parse_layer_type(element))
tile_map = objects.TileMap(parent_dir, version, tiled_version,
orientation, render_order, map_size, tile_size,
infinite, next_layer_id, next_object_id,
tile_sets, layers)
try:
tile_map.hex_side_length = int(map_element.attrib['hexsidelength'])
except KeyError:
pass
try:
tile_map.stagger_axis = int(map_element.attrib['staggeraxis'])
except KeyError:
pass
try:
tile_map.stagger_index = int(map_element.attrib['staggerindex'])
except KeyError:
pass
try:
backgroundcolor = map_element.attrib['backgroundcolor']
except KeyError:
pass
else:
tile_map.background_color = utilities.parse_color(backgroundcolor)
properties_element = map_tree.find('./properties')
if properties_element is not None:
tile_map.properties = _parse_properties_element(properties_element)
return tile_map

View File

@@ -9,7 +9,7 @@ def parse_color(color: str) -> objects.Color:
:Color: Color object in the format that Arcade understands. :Color: Color object in the format that Arcade understands.
""" """
# strip initial '#' character # strip initial '#' character
if not len(color) % 2 == 0: # pylint: disable=C2001 if not len(color) % 2 == 0:
color = color[1:] color = color[1:]
if len(color) == 6: if len(color) == 6:
@@ -25,3 +25,21 @@ def parse_color(color: str) -> objects.Color:
blue = int(color[6:8], 16) blue = int(color[6:8], 16)
return objects.Color(red, green, blue, alpha) return objects.Color(red, green, blue, alpha)
def get_tile_by_gid(tile_sets: objects.TileSetDict, gid: int) -> objects.Tile:
"""Gets Tile from a global tile ID.
Args:
tile_sets (objects.TileSetDict): TileSetDict from TileMap.
gid (int): Global tile ID of the tile to be returned.
Returns:
objects.Tile: The Tile object reffered to by the global tile ID.
"""
for tileset_key, tileset in tile_sets.items():
for tile_key, tile in tileset.tiles.items():
tile_gid = tile.id + tileset_key
if tile_gid == gid:
return tile
return None

View File

@@ -0,0 +1,742 @@
import functools
import base64
import gzip
import re
import zlib
from pathlib import Path
from typing import Callable, Dict, List, Optional, Tuple, Union
import xml.etree.ElementTree as etree
import pytiled_parser.objects as objects
import pytiled_parser.utilities as utilities
def _decode_base64_data(
data_text: str, layer_width: int, compression: Optional[str] = None
) -> List[List[int]]:
tile_grid: List[List[int]] = [[]]
unencoded_data = base64.b64decode(data_text)
if compression == "zlib":
unzipped_data = zlib.decompress(unencoded_data)
elif compression == "gzip":
unzipped_data = gzip.decompress(unencoded_data)
elif compression is None:
unzipped_data = unencoded_data
else:
raise ValueError(f"Unsupported compression type '{compression}'.")
# Turn bytes into 4-byte integers
byte_count = 0
int_count = 0
int_value = 0
row_count = 0
for byte in unzipped_data:
int_value += byte << (byte_count * 8)
byte_count += 1
if byte_count % 4 == 0:
byte_count = 0
int_count += 1
tile_grid[row_count].append(int_value)
int_value = 0
if int_count % layer_width == 0:
row_count += 1
tile_grid.append([])
tile_grid.pop()
return tile_grid
def _decode_csv_data(data_text: str) -> List[List[int]]:
"""Decodes csv encoded layer data.
Credit:
"""
tile_grid = []
lines: List[str] = data_text.split("\n")
# remove erronious empty lists due to a newline being on both ends of text
lines = lines[1:-1]
for line in lines:
line_list = line.split(",")
# FIXME: what is this for?
while "" in line_list:
line_list.remove("")
line_list_int = [int(item) for item in line_list]
tile_grid.append(line_list_int)
return tile_grid
def _decode_data(
element: etree.Element,
layer_width: int,
encoding: str,
compression: Optional[str],
) -> List[List[int]]:
"""Decodes data or chunk data.
Args:
:element (Element): Element to have text decoded.
:layer_width (int): Number of tiles per column in this layer. Used
for determining when to cut off a row when decoding base64
encoding layers.
:encoding (str): Encoding format of the layer data.
:compression (str): Compression format of the layer data.
"""
# etree.Element.text comes with an appended and a prepended '\n'
supported_encodings = ["base64", "csv"]
if encoding not in supported_encodings:
raise ValueError("{encoding} is not a valid encoding")
supported_compression = [None, "gzip", "zlib"]
if compression is not None:
if encoding != "base64":
raise ValueError("{encoding} does not support compression")
if compression not in supported_compression:
raise ValueError("{compression} is not a valid compression type")
try:
data_text: str = element.text # type: ignore
except AttributeError:
raise AttributeError(f"{element} lacks layer data.")
if encoding == "csv":
return _decode_csv_data(data_text)
return _decode_base64_data(data_text, compression, layer_width)
def _parse_data(
element: etree.Element, layer_width: int
) -> objects.LayerData:
"""Parses layer data.
Will parse CSV, base64, gzip-base64, or zlip-base64 encoded data.
Args:
:element (Element): Data element to parse.
:width (int): Layer width. Used for base64 decoding.
Returns:
:LayerData: Data object containing layer data or chunks of data.
"""
encoding = element.attrib["encoding"]
compression = None
try:
compression = element.attrib["compression"]
except KeyError:
pass
chunk_elements = element.findall("./chunk")
if chunk_elements:
chunks: List[objects.Chunk] = []
for chunk_element in chunk_elements:
x = int(chunk_element.attrib["x"])
y = int(chunk_element.attrib["y"])
location = objects.OrderedPair(x, y)
width = int(chunk_element.attrib["width"])
height = int(chunk_element.attrib["height"])
layer_data = _decode_data(
chunk_element, layer_width, encoding, compression
)
chunks.append(objects.Chunk(location, width, height, layer_data))
return chunks
return _decode_data(element, layer_width, encoding, compression)
def _parse_layer(
layer_element: etree.Element
) -> Tuple[
int,
str,
Optional[objects.OrderedPair],
Optional[float],
Optional[objects.Properties],
]:
"""Parses all of the attributes for a Layer object.
Args:
layer_element: The layer element to be parsed.
Returns:
FIXME
"""
id = int(layer_element.attrib["id"])
name = layer_element.attrib["name"]
offset: Optional[objects.OrderedPair]
offset_x_attrib = layer_element.attrib.get("offsetx")
offset_y_attrib = layer_element.attrib.get("offsety")
# If any offset is present, we need to return an OrderedPair
# Unknown if one of the offsets could be absent.
if any([offset_x_attrib, offset_y_attrib]):
if offset_x_attrib:
offset_x = float(offset_x_attrib)
else:
offset_x = 0.0
if offset_y_attrib:
offset_y = float(offset_y_attrib)
else:
offset_y = 0.0
offset = objects.OrderedPair(offset_x, offset_y)
else:
offset = None
opacity: Optional[float]
opacity_attrib = layer_element.attrib.get("opacity")
if opacity_attrib:
opacity = float(opacity_attrib)
else:
opacity = None
properties: Optional[objects.Properties]
properties_element = layer_element.find("./properties")
if properties_element is not None:
properties = _parse_properties_element(properties_element)
else:
properties = None
return id, name, offset, opacity, properties
def _parse_tile_layer(element: etree.Element,) -> objects.TileLayer:
"""Parses tile layer element.
Args:
element: The layer element to be parsed.
Returns:
TileLayer: The tile layer object.
"""
id, name, offset, opacity, properties = _parse_layer(element)
width = int(element.attrib["width"])
height = int(element.attrib["height"])
size = objects.Size(width, height)
data_element = element.find("./data")
if data_element is not None:
data: objects.LayerData = _parse_data(data_element, width)
else:
raise ValueError(f"{element} has no child data element.")
return objects.TileLayer(
id, name, offset, opacity, properties, size, data
)
def _parse_objects(
object_elements: List[etree.Element]
) -> List[objects.TiledObject]:
"""Parses objects found in the 'objectgroup' element.
Args:
object_elements: List of object elements to be parsed.
Returns:
list: List of parsed tiled objects.
"""
tiled_objects: List[objects.TiledObject] = []
for object_element in object_elements:
id = int(object_element.attrib["id"])
location_x = float(object_element.attrib["x"])
location_y = float(object_element.attrib["y"])
location = objects.OrderedPair(location_x, location_y)
tiled_object = objects.TiledObject(id, location)
try:
width = float(object_element.attrib["width"])
except KeyError:
width = 0
try:
height = float(object_element.attrib["height"])
except KeyError:
height = 0
tiled_object.size = objects.Size(width, height)
try:
tiled_object.opacity = float(object_element.attrib["opacity"])
except KeyError:
pass
try:
tiled_object.rotation = int(object_element.attrib["rotation"])
except KeyError:
pass
try:
tiled_object.name = object_element.attrib["name"]
except KeyError:
pass
try:
tiled_object.type = object_element.attrib["type"]
except KeyError:
pass
properties_element = object_element.find("./properties")
if properties_element is not None:
tiled_object.properties = _parse_properties_element(
properties_element
)
tiled_objects.append(tiled_object)
return tiled_objects
def _parse_object_layer(element: etree.Element,) -> objects.ObjectLayer:
"""Parse the objectgroup element given.
Args:
layer_type (objects.LayerType):
id: The id of the layer.
name: The name of the layer.
offset: The offset of the layer.
opacity: The opacity of the layer.
properties: The Properties object of the layer.
Returns:
ObjectLayer: The object layer object.
"""
id, name, offset, opacity, properties = _parse_layer(element)
tiled_objects = _parse_objects(element.findall("./object"))
try:
color = element.attrib["color"]
except KeyError:
pass
try:
draw_order = element.attrib["draworder"]
except KeyError:
pass
return objects.ObjectLayer(
id,
name,
offset,
opacity,
properties,
tiled_objects,
color,
draw_order,
)
def _parse_layer_group(element: etree.Element,) -> objects.LayerGroup:
"""Parse the objectgroup element given.
Args:
layer_type (objects.LayerType):
id: The id of the layer.
name: The name of the layer.
offset: The offset of the layer.
opacity: The opacity of the layer.
properties: The Properties object of the layer.
Returns:
LayerGroup: The layer group object.
"""
id, name, offset, opacity, properties = _parse_layer(element)
layers = _get_layers(element)
return objects.LayerGroup(id, name, offset, opacity, properties, layers)
def _get_layer_parser(
layer_tag: str
) -> Optional[Callable[[etree.Element], objects.Layer]]:
"""Gets a the parser for the layer type specified.
Layer tags are 'layer' for a tile layer, 'objectgroup' for an object
layer, and 'group' for a layer group. If anything else is passed,
returns None.
Args:
layer_tag: Specifies the layer type to be parsed based on the element
tag.
Returns:
Callable: the function to be used to parse the layer.
None: The element is not a map layer.
"""
if layer_tag == "layer":
return _parse_tile_layer
elif layer_tag == "objectgroup":
return _parse_object_layer
elif layer_tag == "group":
return _parse_layer_group
else:
return None
def _get_layers(map_element: etree.Element) -> List[objects.Layer]:
"""Parse layer type element given.
Retains draw order based on the returned lists index FIXME: confirm
Args:
map_element: The element containing the layer.
Returns:
List[Layer]: A list of the layers, ordered by draw order.
FIXME: confirm
"""
layers: List[objects.Layer] = []
for element in map_element.findall("./"):
layer_parser = _get_layer_parser(element.tag)
if layer_parser:
layers.append(layer_parser(element))
return layers
@functools.lru_cache()
def _parse_external_tile_set(
parent_dir: Path, tile_set_element: etree.Element
) -> objects.TileSet:
"""Parses an external tile set.
Caches the results to speed up subsequent maps with identical tilesets.
"""
source = Path(tile_set_element.attrib["source"])
tile_set_tree = etree.parse(str(parent_dir / Path(source))).getroot()
return _parse_tile_set(tile_set_tree)
def _parse_hitboxes(element: etree.Element) -> List[objects.TiledObject]:
"""Parses all hitboxes for a given tile."""
return _parse_objects(element.findall("./object"))
def _parse_tiles(
tile_element_list: List[etree.Element]
) -> Dict[int, objects.Tile]:
tiles: Dict[int, objects.Tile] = {}
for tile_element in tile_element_list:
# id is not optional
id = int(tile_element.attrib["id"])
# optional attributes
tile_type = None
try:
tile_type = tile_element.attrib["type"]
except KeyError:
pass
tile_terrain = None
try:
tile_terrain_attrib = tile_element.attrib["terrain"]
except KeyError:
pass
else:
# below is an attempt to explain how terrains are handled.
#'terrain' attribute is a comma seperated list of 4 values,
# each is either an integer or blank
# convert to list of values
terrain_list_attrib = re.split(",", tile_terrain_attrib)
# terrain_list is list of indexes of Tileset.terrain_types
terrain_list: List[Optional[int]] = []
# each index in terrain_list_attrib reffers to a corner
for corner in terrain_list_attrib:
if corner == "":
terrain_list.append(None)
else:
terrain_list.append(int(corner))
tile_terrain = objects.TileTerrain(*terrain_list)
# tile element optional sub-elements
animation: Optional[List[objects.Frame]] = None
tile_animation_element = tile_element.find("./animation")
if tile_animation_element:
animation = []
frames = tile_animation_element.findall("./frame")
for frame in frames:
# tileid reffers to the Tile.id of the animation frame
tile_id = int(frame.attrib["tileid"])
# duration is in MS. Should perhaps be converted to seconds.
# FIXME: make decision
duration = int(frame.attrib["duration"])
animation.append(objects.Frame(tile_id, duration))
# if this is None, then the Tile is part of a spritesheet
tile_image = None
tile_image_element = tile_element.find("./image")
if tile_image_element is not None:
tile_image = _parse_image_element(tile_image_element)
hitboxes = None
tile_hitboxes_element = tile_element.find("./objectgroup")
if tile_hitboxes_element is not None:
hitboxes = _parse_hitboxes(tile_hitboxes_element)
tiles[id] = objects.Tile(
id, tile_type, tile_terrain, animation, tile_image, hitboxes
)
return tiles
def _parse_image_element(image_element: etree.Element) -> objects.Image:
"""Parse image element given.
Returns:
: Color in Arcade's preffered format.
"""
image = objects.Image(image_element.attrib["source"])
width_attrib = image_element.attrib.get("width")
height_attrib = image_element.attrib.get("height")
if width_attrib and height_attrib:
image.size = objects.Size(int(width_attrib), int(height_attrib))
try:
image.trans = image_element.attrib["trans"]
except KeyError:
pass
return image
def _parse_properties_element(
properties_element: etree.Element
) -> objects.Properties:
"""Adds Tiled property to Properties dict.
Args:
:name (str): Name of property.
:property_type (str): Type of property. Can be string, int, float,
bool, color or file. Defaults to string.
:value (str): The value of the property.
Returns:
:Properties: Properties Dict object.
"""
properties: objects.Properties = {}
for property_element in properties_element.findall("./property"):
name = property_element.attrib["name"]
try:
property_type = property_element.attrib["type"]
except KeyError:
# strings do not have an attribute in property elements
property_type = "string"
value = property_element.attrib["value"]
property_types = ["string", "int", "float", "bool", "color", "file"]
assert (
property_type in property_types
), f"Invalid type for property {name}"
if property_type == "int":
properties[name] = int(value)
elif property_type == "float":
properties[name] = float(value)
elif property_type == "color":
properties[name] = value
elif property_type == "file":
properties[name] = Path(value)
elif property_type == "bool":
if value == "true":
properties[name] = True
else:
properties[name] = False
else:
properties[name] = value
return properties
def _parse_tile_set(tile_set_element: etree.Element) -> objects.TileSet:
"""
Parses a tile set that is embedded into a TMX.
"""
# get all basic attributes
name = tile_set_element.attrib["name"]
max_tile_width = int(tile_set_element.attrib["tilewidth"])
max_tile_height = int(tile_set_element.attrib["tileheight"])
max_tile_size = objects.Size(max_tile_width, max_tile_height)
spacing = None
try:
spacing = int(tile_set_element.attrib["spacing"])
except KeyError:
pass
margin = None
try:
margin = int(tile_set_element.attrib["margin"])
except KeyError:
pass
tile_count = None
try:
tile_count = int(tile_set_element.attrib["tilecount"])
except KeyError:
pass
columns = None
try:
columns = int(tile_set_element.attrib["columns"])
except KeyError:
pass
tile_offset = None
tileoffset_element = tile_set_element.find("./tileoffset")
if tileoffset_element is not None:
tile_offset_x = int(tileoffset_element.attrib["x"])
tile_offset_y = int(tileoffset_element.attrib["y"])
tile_offset = objects.OrderedPair(tile_offset_x, tile_offset_y)
grid = None
grid_element = tile_set_element.find("./grid")
if grid_element is not None:
grid_orientation = grid_element.attrib["orientation"]
grid_width = int(grid_element.attrib["width"])
grid_height = int(grid_element.attrib["height"])
grid = objects.Grid(grid_orientation, grid_width, grid_height)
properties = None
properties_element = tile_set_element.find("./properties")
if properties_element is not None:
properties = _parse_properties_element(properties_element)
terrain_types: Optional[List[objects.Terrain]] = None
terrain_types_element = tile_set_element.find("./terraintypes")
if terrain_types_element is not None:
terrain_types = []
for terrain in terrain_types_element.findall("./terrain"):
name = terrain.attrib["name"]
terrain_tile = int(terrain.attrib["tile"])
terrain_types.append(objects.Terrain(name, terrain_tile))
image = None
image_element = tile_set_element.find("./image")
if image_element is not None:
image = _parse_image_element(image_element)
tile_element_list = tile_set_element.findall("./tile")
tiles = _parse_tiles(tile_element_list)
return objects.TileSet(
name,
max_tile_size,
spacing,
margin,
tile_count,
columns,
tile_offset,
grid,
properties,
image,
terrain_types,
tiles,
)
def parse_tile_map(tmx_file: Union[str, Path]) -> objects.TileMap:
# setting up XML parsing
map_tree = etree.parse(str(tmx_file))
map_element = map_tree.getroot()
# positional arguments for TileMap
parent_dir = Path(tmx_file).parent
version = map_element.attrib["version"]
tiled_version = map_element.attrib["tiledversion"]
orientation = map_element.attrib["orientation"]
render_order = map_element.attrib["renderorder"]
map_width = int(map_element.attrib["width"])
map_height = int(map_element.attrib["height"])
map_size = objects.Size(map_width, map_height)
tile_width = int(map_element.attrib["tilewidth"])
tile_height = int(map_element.attrib["tileheight"])
tile_size = objects.Size(tile_width, tile_height)
infinite_attribute = map_element.attrib["infinite"]
infinite = True if infinite_attribute == "true" else False
next_layer_id = int(map_element.attrib["nextlayerid"])
next_object_id = int(map_element.attrib["nextobjectid"])
# parse all tilesets
tile_sets: Dict[int, objects.TileSet] = {}
tile_set_element_list = map_element.findall("./tileset")
for tile_set_element in tile_set_element_list:
# tiled docs are ambiguous about the 'firstgid' attribute
# current understanding is for the purposes of mapping the layer
# data to the tile set data, add the 'firstgid' value to each
# tile 'id'; this means that the 'firstgid' is specific to each,
# tile set as they pertain to the map, not tile set specific as
# the tiled docs can make it seem
# 'firstgid' is saved beside each TileMap
firstgid = int(tile_set_element.attrib["firstgid"])
try:
# check if is an external TSX
source = tile_set_element.attrib["source"]
except KeyError:
# the tile set in embedded
name = tile_set_element.attrib["name"]
tile_sets[firstgid] = _parse_tile_set(tile_set_element)
else:
# tile set is external
tile_sets[firstgid] = _parse_external_tile_set(
parent_dir, tile_set_element
)
layers = _get_layers(map_element)
tile_map = objects.TileMap(
parent_dir,
version,
tiled_version,
orientation,
render_order,
map_size,
tile_size,
infinite,
next_layer_id,
next_object_id,
tile_sets,
layers,
)
try:
tile_map.hex_side_length = int(map_element.attrib["hexsidelength"])
except KeyError:
pass
try:
tile_map.stagger_axis = int(map_element.attrib["staggeraxis"])
except KeyError:
pass
try:
tile_map.stagger_index = int(map_element.attrib["staggerindex"])
except KeyError:
pass
try:
tile_map.background_color = map_element.attrib["backgroundcolor"]
except KeyError:
pass
properties_element = map_tree.find("./properties")
if properties_element is not None:
tile_map.properties = _parse_properties_element(properties_element)
return tile_map

View File

@@ -3,38 +3,36 @@ import sys
from setuptools import setup from setuptools import setup
BUILD = 0 BUILD = 0
VERSION = '0.1' VERSION = "0.0.1"
RELEASE = VERSION RELEASE = VERSION
if __name__ == '__main__': if __name__ == "__main__":
readme = path.join(path.dirname(path.abspath(__file__)), 'README.md') readme = path.join(path.dirname(path.abspath(__file__)), "README.md")
with open(readme, 'r') as f: with open(readme, "r") as f:
long_desc = f.read() long_desc = f.read()
setup( setup(
name='pytiled_parser', name="pytiled_parser",
version=RELEASE, version=RELEASE,
description='Python Library for parsing Tiled Map Editor maps.', description="Python Library for parsing Tiled Map Editor maps.",
long_description=long_desc, long_description=long_desc,
author='Benjamin Kirkbride', author="Benjamin Kirkbride",
author_email='BenjaminKirkbride@gmail.com', author_email="BenjaminKirkbride@gmail.com",
license='MIT', license="MIT",
url='https://github.com/Beefy-Swain/pytiled_parser', url="https://github.com/Beefy-Swain/pytiled_parser",
download_url='https://github.com/Beefy-Swain/pytiled_parser', download_url="https://github.com/Beefy-Swain/pytiled_parser",
install_requires=[ install_requires=["dataclasses"],
'dataclasses', packages=["pytiled_parser"],
], classifiers=[
packages=['pytiled_parser'], "Development Status :: 1 - Planning",
classifiers=[ "Intended Audience :: Developers",
'Development Status :: 1 - Planning', "License :: OSI Approved :: MIT License",
'Intended Audience :: Developers', "Operating System :: OS Independent",
'License :: OSI Approved :: MIT License', "Programming Language :: Python",
'Operating System :: OS Independent', "Programming Language :: Python :: 3.6",
'Programming Language :: Python', "Programming Language :: Python :: 3.7",
'Programming Language :: Python :: 3.6', "Programming Language :: Python :: Implementation :: CPython",
'Programming Language :: Python :: 3.7', "Topic :: Software Development :: Libraries :: Python Modules",
'Programming Language :: Python :: Implementation :: CPython', ],
'Topic :: Software Development :: Libraries :: Python Modules', test_suite="tests",
], )
test_suite='tests',
)

View File

@@ -1,25 +1,20 @@
{ 'background_color': None, { 'background_color': None,
'height': 6,
'hex_side_length': None, 'hex_side_length': None,
'infinite': False, 'infinite': False,
'layers': [ Layer(width=8, height=6, data=[[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]], id=1, name='Tile Layer 1', offset=OrderedPair(x=0, y=0), opacity=255, properties=None)], 'layers': [ TileLayer(id=1, name='Tile Layer 1', offset=None, opacity=None, properties=None, size=Size(width=10, height=10), data=[[1, 2, 3, 4, 5, 6, 7, 8, 30, 30], [9, 10, 11, 12, 13, 14, 15, 16, 30, 30], [17, 18, 19, 20, 21, 22, 23, 24, 30, 30], [25, 26, 27, 28, 29, 30, 31, 32, 30, 30], [33, 34, 35, 36, 37, 38, 39, 40, 30, 30], [41, 42, 43, 44, 45, 46, 47, 48, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30]]),
'next_layer_id': 2, TileLayer(id=2, name='Tile Layer 2', offset=None, opacity=0.5, properties=None, size=Size(width=10, height=10), data=[[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 46, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 6, 7, 7, 7, 7, 7, 8, 0], [0, 0, 14, 15, 15, 15, 15, 15, 16, 0], [0, 0, 22, 23, 23, 23, 23, 23, 24, 0]]),
'next_object_id': 1, LayerGroup(id=3, name='Group 1', offset=None, opacity=None, properties={'bool property': True}, layers=[TileLayer(id=5, name='Tile Layer 4', offset=OrderedPair(x=49.0, y=-50.0), opacity=None, properties=None, size=Size(width=10, height=10), data=[[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 31, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]), TileLayer(id=4, name='Tile Layer 3', offset=None, opacity=None, properties=None, size=Size(width=10, height=10), data=[[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [1, 2, 3, 0, 0, 0, 0, 0, 0, 0], [9, 10, 11, 0, 0, 0, 0, 0, 0, 0], [17, 18, 19, 0, 0, 0, 0, 0, 0, 0]])]),
ObjectLayer(id=6, name='Object Layer 1', offset=OrderedPair(x=4.66667, y=-4.33333), opacity=0.9, properties=None, tiled_objects=[TiledObject(id=1, location=OrderedPair(x=200.25, y=210.75), size=Size(width=47.25, height=25.0), rotation=15, opacity=1, name='rectangle 1', type='rectangle type', properties=None, template=None), TiledObject(id=2, location=OrderedPair(x=252.5, y=87.75), size=Size(width=0, height=0), rotation=-21, opacity=1, name='polygon 1', type='polygon type', properties=None, template=None), TiledObject(id=3, location=OrderedPair(x=198.75, y=102.5), size=Size(width=17.75, height=14.25), rotation=0, opacity=1, name='elipse 1', type='elipse type', properties=None, template=None), TiledObject(id=4, location=OrderedPair(x=174.25, y=186.0), size=Size(width=0, height=0), rotation=0, opacity=1, name='point 1', type='point type', properties=None, template=None), TiledObject(id=7, location=OrderedPair(x=11.3958, y=48.5833), size=Size(width=107.625, height=27.25), rotation=0, opacity=1, name='insert text 1', type='insert text type', properties=None, template=None), TiledObject(id=6, location=OrderedPair(x=47.25, y=72.5), size=Size(width=47.0, height=53.0), rotation=31, opacity=1, name='inserted tile 1', type='inserted tile type', properties={'tile property bool': True}, template=None), TiledObject(id=8, location=OrderedPair(x=144.667, y=112.0), size=Size(width=0, height=0), rotation=0, opacity=1, name='polyline 1', type='polyline type', properties=None, template=None), TiledObject(id=9, location=OrderedPair(x=69.8333, y=168.333), size=Size(width=0, height=0), rotation=0, opacity=1, name='polygon 2', type='polygon type', properties=None, template=None)], color=Color(red=0, green=0, blue=0, alpha=255), draw_order='index')],
'map_size': Size(width=10, height=10),
'next_layer_id': 16,
'next_object_id': 10,
'orientation': 'orthogonal', 'orientation': 'orthogonal',
'parent_dir': PosixPath('/home/ben/Projects/pytiled_parser/pytiled_parser-venv/pytiled_parser/tests/test_data'), 'parent_dir': PosixPath('/home/benk/Projects/pytiled_parser/venv/pytiled_parser/tests/test_data'),
'properties': { 'bool property - false': False, 'properties': None,
'bool property - true': True,
'color property': Color(red=73, green=252, blue=255, alpha=255),
'file property': PosixPath('/var/log/syslog'),
'float property': 1.23456789,
'int property': 13,
'string property': 'Hello, World!!'},
'render_order': 'right-down', 'render_order': 'right-down',
'stagger_axis': None, 'stagger_axis': None,
'stagger_index': None, 'stagger_index': None,
'tile_height': 32, 'tile_sets': { 1: TileSet(name='tile_set_image', max_tile_size=Size(width=32, height=32), spacing=1, margin=1, tile_count=48, columns=8, tile_offset=None, grid=None, properties=None, image=Image(source='images/tmw_desert_spacing.png', size=Size(width=265, height=199), trans=None), terrain_types=None, tiles={})},
'tile_sets': { 1: TileSet(name='tile_set_image', max_tile_size=OrderedPair(x=32, y=32), spacing=1, margin=1, tile_count=48, columns=8, tile_offset=None, grid=None, properties=None, image=Image(source='images/tmw_desert_spacing.png', trans=None, width=265, height=199), terrain_types=None, tiles={})}, 'tile_size': Size(width=32, height=32),
'tile_width': 32,
'tiled_version': '1.2.3', 'tiled_version': '1.2.3',
'version': '1.2', 'version': '1.2'}
'width': 8}

View File

@@ -10,7 +10,7 @@ pp = pprint.PrettyPrinter(indent=4, compact=True, width=100)
pp = pp.pprint pp = pp.pprint
MAP_NAME = '/home/ben/Projects/pytiled_parser/pytiled_parser-venv/pytiled_parser/tests/test_data/test_map_simple.tmx' MAP_NAME = "/home/benk/Projects/pytiled_parser/venv/pytiled_parser/tests/test_data/test_map_image_tile_set.tmx"
map = pytiled_parser.parse_tile_map(MAP_NAME) map = pytiled_parser.parse_tile_map(MAP_NAME)

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.2" tiledversion="1.2.3" orientation="orthogonal" renderorder="right-down" 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="../../../../../../../../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="tile_set_image_hitboxes.tsx"/>
<layer id="1" name="Tile Layer 1" width="8" height="6">
<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>
</map>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.2" tiledversion="1.2.3" orientation="orthogonal" renderorder="right-down" 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="../../../../../../../../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="tile_set_image.tsx"/>
<layer id="1" name="Tile Layer 1" width="8" height="6">
<data encoding="base64" compression="gzip">
H4sIAAAAAAAAAw3DBRKCQAAAwDMRA7BQLMTE9v+vY3dmWyGEth279uwbOTB26MixExNTM6fOnLtwae7KtYUbt+7ce7D0aOXJsxev3rxb+/Dpy7cfv/782wAcvDirwAAAAA==
</data>
</layer>
</map>

View File

@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<tileset version="1.2" tiledversion="1.2.3" 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"/>
<tile id="9">
<objectgroup draworder="index">
<object id="2" name="wall" type="rectangle type" x="1" y="1" width="32" height="32" rotation="1"/>
</objectgroup>
</tile>
<tile id="19">
<objectgroup draworder="index">
<object id="1" name="wall corner" type="polygon type" x="32" y="1" rotation="1">
<polygon points="0,0 -32,0 -32,32 -16,32.1818 -15.8182,16.9091 0.181818,17.0909"/>
</object>
</objectgroup>
</tile>
<tile id="20">
<objectgroup draworder="index">
<object id="1" name="polyline" type="polyline type" x="1.45455" y="1.45455" rotation="1">
<polyline points="0,0 25.0909,21.2727 9.63636,28.3636"/>
</object>
</objectgroup>
</tile>
<tile id="31">
<objectgroup draworder="index">
<object id="1" name="rock 1" type="elipse type" x="5.09091" y="2.54545" width="19.6364" height="19.2727" rotation="1">
<ellipse/>
</object>
<object id="2" name="rock 2" type="elipse type" x="16.1818" y="22" width="8.54545" height="8.36364" rotation="-1">
<ellipse/>
</object>
</objectgroup>
</tile>
<tile id="45">
<objectgroup draworder="index">
<object id="1" name="sign" type="point type" x="14.7273" y="26.3636">
<point/>
</object>
</objectgroup>
</tile>
</tileset>

170
tests/unit2/test_parser.py Normal file
View File

@@ -0,0 +1,170 @@
import pytest
import xml.etree.ElementTree as etree
from contextlib import contextmanager
from typing import Callable, List, Optional, Tuple
from pytiled_parser import objects, xml_parser, utilities
@contextmanager
def does_not_raise():
yield
def _get_root_element(xml: str) -> etree.Element:
return etree.fromstring(xml)
layer_data = [
(
'<layer id="1" name="Tile Layer 1" width="10" height="10">'
"</layer>",
(int(1), "Tile Layer 1", None, None, None),
),
(
'<layer id="2" name="Tile Layer 2" width="10" height="10" opacity="0.5">'
"</layer>",
(int(2), "Tile Layer 2", None, float(0.5), None),
),
(
'<layer id="5" name="Tile Layer 4" width="10" height="10" offsetx="49" offsety="-50">'
"<properties>"
"</properties>"
"</layer>",
(
int(5),
"Tile Layer 4",
objects.OrderedPair(49, -50),
None,
"properties",
),
),
]
@pytest.mark.parametrize("xml,expected", layer_data)
def test_parse_layer(xml, expected, monkeypatch):
def mockreturn(properties):
return "properties"
monkeypatch.setattr(xml_parser, "_parse_properties_element", mockreturn)
assert xml_parser._parse_layer(_get_root_element(xml)) == expected
@pytest.mark.parametrize(
"test_input,expected",
[
("#001122", (0x00, 0x11, 0x22, 0xFF)),
("001122", (0x00, 0x11, 0x22, 0xFF)),
("#FF001122", (0x00, 0x11, 0x22, 0xFF)),
("FF001122", (0x00, 0x11, 0x22, 0xFF)),
("FF001122", (0x00, 0x11, 0x22, 0xFF)),
],
)
def test_color_parsing(test_input, expected):
"""
Tiled has a few different types of color representations.
"""
assert utilities.parse_color(test_input) == expected
data_csv = [
(
"\n1,2,3,4,5,6,7,8,\n"
"9,10,11,12,13,14,15,16,\n"
"17,18,19,20,21,22,23,24,\n"
"25,26,27,28,29,30,31,32,\n"
"33,34,35,36,37,38,39,40,\n"
"41,42,43,44,45,46,47,48\n",
[
[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],
],
),
("\n0,0,0,0,0\n", [[0, 0, 0, 0, 0]]),
]
@pytest.mark.parametrize("data_csv,expected", data_csv)
def test_decode_csv_data(data_csv, expected):
assert xml_parser._decode_csv_data(data_csv) == expected
data_base64 = [
(
"AQAAAAIAAAADAAAABAAAAAUAAAAGAAAABwAAAAgAAAAJAAAACgAAAAsAAAAMAAAADQAAAA4AAAAPAAAAEAAAABEAAAASAAAAEwAAABQAAAAVAAAAFgAAABcAAAAYAAAAGQAAABoAAAAbAAAAHAAAAB0AAAAeAAAAHwAAACAAAAAhAAAAIgAAACMAAAAkAAAAJQAAACYAAAAnAAAAKAAAACkAAAAqAAAAKwAAACwAAAAtAAAALgAAAC8AAAAwAAAA",
8,
None,
[
[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],
],
does_not_raise(),
),
(
"eJwNwwUSgkAAAMAzEQOwUCzExPb/r2N3ZlshhLYdu/bsGzkwdujIsRMTUzOnzpy7cGnuyrWFG7fu3Huw9GjlybMXr968W/vw6cu3H7/+/NsAMw8EmQ==",
8,
"zlib",
[
[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],
],
does_not_raise(),
),
(
"H4sIAAAAAAAAAw3DBRKCQAAAwDMRA7BQLMTE9v+vY3dmWyGEth279uwbOTB26MixExNTM6fOnLtwae7KtYUbt+7ce7D0aOXJsxev3rxb+/Dpy7cfv/782wAcvDirwAAAAA==",
8,
"gzip",
[
[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],
],
does_not_raise(),
),
(
"SGVsbG8gV29ybGQh",
8,
"lzma",
[
[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],
],
pytest.raises(ValueError),
),
]
@pytest.mark.parametrize(
"data_base64,width,compression,expected,raises", data_base64
)
def test_decode_base64_data(
data_base64, width, compression, expected, raises
):
with raises:
assert (
xml_parser._decode_base64_data(data_base64, width, compression)
== expected
)

View File

@@ -15,7 +15,9 @@ def test_map_simple():
""" """
TMX with a very simple spritesheet tile set and some properties. TMX with a very simple spritesheet tile set and some properties.
""" """
map = pytiled_parser.parse_tile_map(Path("../test_data/test_map_simple.tmx")) map = pytiled_parser.parse_tile_map(
Path("../test_data/test_map_simple.tmx")
)
# map # map
# unsure how to get paths to compare propperly # unsure how to get paths to compare propperly
@@ -37,13 +39,13 @@ def test_map_simple():
assert map.background_color == None assert map.background_color == None
assert map.properties == { assert map.properties == {
"bool property - false": False, "bool property - false": False,
"bool property - true": True, "bool property - true": True,
"color property": (0x49, 0xfc, 0xff, 0xff), "color property": (0x49, 0xFC, 0xFF, 0xFF),
"file property": Path("/var/log/syslog"), "file property": Path("/var/log/syslog"),
"float property": 1.23456789, "float property": 1.23456789,
"int property": 13, "int property": 13,
"string property": "Hello, World!!" "string property": "Hello, World!!",
} }
# tileset # tileset
@@ -59,7 +61,8 @@ def test_map_simple():
# unsure how to get paths to compare propperly # unsure how to get paths to compare propperly
assert str(map.tile_sets[1].image.source) == ( assert str(map.tile_sets[1].image.source) == (
"images/tmw_desert_spacing.png") "images/tmw_desert_spacing.png"
)
assert map.tile_sets[1].image.trans == None assert map.tile_sets[1].image.trans == None
assert map.tile_sets[1].image.size == (265, 199) assert map.tile_sets[1].image.size == (265, 199)
@@ -67,28 +70,31 @@ def test_map_simple():
assert map.tile_sets[1].tiles == {} assert map.tile_sets[1].tiles == {}
# layers # layers
assert map.layers[0].data == [[1,2,3,4,5,6,7,8], assert map.layers[0].data == [
[9,10,11,12,13,14,15,16], [1, 2, 3, 4, 5, 6, 7, 8],
[17,18,19,20,21,22,23,24], [9, 10, 11, 12, 13, 14, 15, 16],
[25,26,27,28,29,30,31,32], [17, 18, 19, 20, 21, 22, 23, 24],
[33,34,35,36,37,38,39,40], [25, 26, 27, 28, 29, 30, 31, 32],
[41,42,43,44,45,46,47,48]] [33, 34, 35, 36, 37, 38, 39, 40],
[41, 42, 43, 44, 45, 46, 47, 48],
]
assert map.layers[0].id == 1 assert map.layers[0].id == 1
assert map.layers[0].name == "Tile Layer 1" assert map.layers[0].name == "Tile Layer 1"
assert map.layers[0].offset == (0, 0) assert map.layers[0].offset == None
assert map.layers[0].opacity == 0xFF assert map.layers[0].opacity == None
assert map.layers[0].properties == None assert map.layers[0].properties == None
assert map.layers[0].size == (8, 6) assert map.layers[0].size == (8, 6)
@pytest.mark.parametrize( @pytest.mark.parametrize(
"test_input,expected", [ "test_input,expected",
("#001122", (0x00, 0x11, 0x22, 0xff)), [
("001122", (0x00, 0x11, 0x22, 0xff)), ("#001122", (0x00, 0x11, 0x22, 0xFF)),
("#FF001122", (0x00, 0x11, 0x22, 0xff)), ("001122", (0x00, 0x11, 0x22, 0xFF)),
("FF001122", (0x00, 0x11, 0x22, 0xff)), ("#FF001122", (0x00, 0x11, 0x22, 0xFF)),
("FF001122", (0x00, 0x11, 0x22, 0xff)), ("FF001122", (0x00, 0x11, 0x22, 0xFF)),
] ("FF001122", (0x00, 0x11, 0x22, 0xFF)),
],
) )
def test_color_parsing(test_input, expected): def test_color_parsing(test_input, expected):
""" """