From b6dd3c9510e771110ee81fe603155d17ad82a7cf Mon Sep 17 00:00:00 2001 From: Benjamin Kirkbride Date: Mon, 6 May 2019 18:35:45 -0400 Subject: [PATCH 1/5] added parsing of hitboxes --- pytiled_parser/objects.py | 86 ++++++++++-------- pytiled_parser/parser.py | 96 +++++++++++---------- pytiled_parser/utilities.py | 20 ++++- tests/test_data/tile_set_image_hitboxes.tsx | 40 +++++++++ tests/unit2/test_pytiled_parser.py | 3 +- 5 files changed, 160 insertions(+), 85 deletions(-) create mode 100644 tests/test_data/tile_set_image_hitboxes.tsx diff --git a/pytiled_parser/objects.py b/pytiled_parser/objects.py index 086a9af..6272a37 100644 --- a/pytiled_parser/objects.py +++ b/pytiled_parser/objects.py @@ -11,30 +11,23 @@ from pathlib import Path 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. - """ + """Tmx layer encoding is of an unknown type.""" class TileNotFoundError(Exception): - """ - Tile not found in tileset. - """ + """Tile not found in tileset.""" class ImageNotFoundError(Exception): - """ - Image not found. - """ + """Image not found.""" class Color(NamedTuple): - """ - Color object. + """Color object. Attributes: :red (int): Red, between 1 and 255. @@ -49,17 +42,27 @@ class Color(NamedTuple): class OrderedPair(NamedTuple): - """ - OrderedPair NamedTuple. + """OrderedPair NamedTuple. Attributes: - :x (Union[int, float]): X coordinate. - :y (Union[int, float]): Y coordinate. + x (Union[int, float]): X coordinate. + y (Union[int, float]): Y coordinate. """ x: 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: """ FIXME TODO @@ -102,7 +105,7 @@ class Image(NamedTuple): :height (Optional[str]): The image height in pixels (optional). """ source: str - size: OrderedPair + size: Size trans: Optional[Color] @@ -222,7 +225,7 @@ Either a 2 dimensional array of integers representing the global tile IDs @dataclasses.dataclass class _LayerBase: - size: OrderedPair + size: Size data: LayerData @@ -234,7 +237,7 @@ class Layer(LayerType, _LayerBase): 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 + :size (Size): 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 @@ -250,7 +253,7 @@ class _ObjectBase: @dataclasses.dataclass class _ObjectDefaults: - size: OrderedPair = OrderedPair(0, 0) + size: Size = Size(0, 0) rotation: int = 0 opacity: int = 0xFF @@ -266,15 +269,15 @@ class Object(_ObjectDefaults, _ObjectBase): """ ObjectGroup Object. - See: \ - https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#object + See: + https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#object Args: :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 gets the same ID. :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)). :rotation (int): The rotation of the object in degrees clockwise (default: 0). @@ -355,8 +358,8 @@ class PolylineObject(Object, _PointsObjectBase): """ Polyline defined by a set of connections between points. - See: \ -https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#polyline + See: + https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#polyline Attributes: :points (List[Tuple[int, int]]): List of coordinates relative to \ @@ -444,10 +447,12 @@ https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#objectgroup """ +@dataclasses.dataclass class _LayerGroupBase(_LayerTypeBase): layers: Optional[List[LayerType]] +@dataclasses.dataclass class LayerGroup(LayerType): """ Layer Group. @@ -464,6 +469,12 @@ class LayerGroup(LayerType): """ +@dataclasses.dataclass +class Hitbox: + """Group of hitboxes for + """ + + @dataclasses.dataclass class Tile: """ @@ -482,7 +493,7 @@ class Tile: terrain: Optional[TileTerrain] animation: Optional[List[Frame]] image: Optional[Image] - hit_box: Optional[List[Object]] + hitboxes: Optional[List[Object]] @dataclasses.dataclass @@ -492,7 +503,7 @@ class TileSet: Args: :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. :spacing (int): The spacing in pixels between the tiles in this tileset (applies to the tileset image). @@ -516,7 +527,7 @@ class TileSet: :tiles (Optional[Dict[int, Tile]]): Dict of Tile objects by Tile.id. """ name: str - max_tile_size: OrderedPair + max_tile_size: Size spacing: Optional[int] margin: Optional[int] tile_count: Optional[int] @@ -529,6 +540,9 @@ class TileSet: tiles: Optional[Dict[int, Tile]] +TileSetDict = Dict[int, TileSet] + + @dataclasses.dataclass class TileMap: """ @@ -548,8 +562,8 @@ class TileMap: rendered. Valid values are right-down, right-up, left-down and left-up. In all cases, the map is drawn row-by-row. (only supported for orthogonal maps at the moment) - :map_size (OrderedPair): The map width in tiles. - :tile_size (OrderedPair): The width of a tile. + :map_size (Size): The map width in tiles. + :tile_size (Size): The width of a tile. :infinite (bool): If the map is infinite or not. :hexsidelength (int): Only for hexagonal maps. Determines the width or height (depending on the staggered axis) of the tile’s edge, in @@ -563,8 +577,8 @@ class TileMap: :nextlayerid (int): Stores the next available ID for new layers. :nextobjectid (int): Stores the next available ID for new objects. :tile_sets (dict[str, TileSet]): Dict of tile sets used - in this map. Key is the source for external tile sets or the name - for embedded ones. The value is a TileSet object. + in this map. Key is the first GID for the tile set. The value + is a TileSet object. :layers List[LayerType]: List of layer objects by draw order. """ parent_dir: Path @@ -573,13 +587,13 @@ class TileMap: tiled_version: str orientation: str render_order: str - map_size: OrderedPair - tile_size: OrderedPair + map_size: Size + tile_size: Size infinite: bool next_layer_id: int next_object_id: int - tile_sets: Dict[int, TileSet] + tile_sets: TileSetDict layers: List[LayerType] hex_side_length: Optional[int] = None @@ -607,5 +621,3 @@ class TileMap: [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) ''' - -#buffer diff --git a/pytiled_parser/parser.py b/pytiled_parser/parser.py index 462489f..5a49f72 100644 --- a/pytiled_parser/parser.py +++ b/pytiled_parser/parser.py @@ -1,17 +1,19 @@ import functools -import re import base64 +import gzip +import re import zlib from pathlib import Path -from typing import * +from typing import Dict, List, Optional, Union 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]] = [[]] @@ -47,8 +49,7 @@ def _decode_base64_data(data_text, compression, layer_width): def _decode_csv_layer(data_text): - """ - Decodes csv encoded layer data. + """Decodes csv encoded layer data. Credit: """ @@ -69,8 +70,7 @@ def _decode_csv_layer(data_text): def _decode_data(element: etree.Element, layer_width: int, encoding: str, compression: Optional[str]) -> List[List[int]]: - """ - Decodes data or chunk data. + """Decodes data or chunk data. Args: :element (Element): Element to have text decoded. @@ -105,8 +105,7 @@ def _decode_data(element: etree.Element, layer_width: int, encoding: str, def _parse_data(element: etree.Element, layer_width: int) -> objects.LayerData: - """ - Parses layer data. + """Parses layer data. Will parse CSV, base64, gzip-base64, or zlip-base64 encoded data. @@ -143,9 +142,7 @@ def _parse_data(element: etree.Element, def _parse_layer(element: etree.Element, layer_type: objects.LayerType) -> objects.Layer: - """ - Parse layer element given. - """ + """Parse layer element given.""" width = int(element.attrib['width']) height = int(element.attrib['height']) size = objects.OrderedPair(width, height) @@ -159,9 +156,7 @@ def _parse_layer(element: etree.Element, def _parse_layer_type(layer_element: etree.Element) -> objects.LayerType: - """ - Parse layer type element given. - """ + """Parse layer type element given.""" id = int(layer_element.attrib['id']) name = layer_element.attrib['name'] @@ -198,13 +193,10 @@ def _parse_layer_type(layer_element: etree.Element) -> objects.LayerType: # return _parse_layer_group(layer_element, layer_type_object) -def _parse_object_group(element: etree.Element, - layer_type: objects.LayerType) -> objects.ObjectGroup: +def _parse_objects(objects: List[etree.Element]) -> List[objects.Object]: """ - Parse object group element given. """ - object_elements = element.findall('./object') - tile_objects: List[objects.Object] = [] + tiled_objects: List[objects.Object] = [] for object_element in object_elements: id = int(object_element.attrib['id']) @@ -246,9 +238,21 @@ def _parse_object_group(element: etree.Element, if properties_element is not None: object.properties = _parse_properties_element(properties_element) - tile_objects.append(object) + tiled_objects.append(object) - object_group = objects.ObjectGroup(tile_objects, **layer_type.__dict__) + return tiled_objects + + +def _parse_object_group(element: etree.Element, + layer_type: objects.LayerType) -> objects.ObjectGroup: + """Parse the objectgroup element given. + + Args: + layer_type (objects.LayerType): + """ + tiled_objects = _parse_objects(element.findall('./object')) + + object_group = objects.ObjectGroup(tiled_objects, **layer_type.__dict__) try: color = utilities.parse_color(element.attrib['color']) except KeyError: @@ -265,8 +269,7 @@ def _parse_object_group(element: etree.Element, @functools.lru_cache() def _parse_external_tile_set(parent_dir: Path, tile_set_element: etree.Element ) -> objects.TileSet: - """ - Parses an external tile set. + """Parses an external tile set. Caches the results to speed up subsequent instances. """ @@ -276,17 +279,21 @@ def _parse_external_tile_set(parent_dir: Path, tile_set_element: etree.Element return _parse_tile_set(tile_set_tree) +def _parse_hitboxes(element: etree.Element) -> List[objects.Object]: + 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 is not optional id = int(tile_element.attrib['id']) - # optional attributes - type = None + #optional attributes + tile_type = None try: - type = tile_element.attrib['type'] + tile_type = tile_element.attrib['type'] except KeyError: pass @@ -296,15 +303,15 @@ def _parse_tiles(tile_element_list: List[etree.Element] 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 + #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 + #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 is list of indexes of Tileset.terrain_types terrain_list: List[Optional[int]] = [] - # each index in terrain_list_attrib reffers to a corner + #each index in terrain_list_attrib reffers to a corner for corner in terrain_list_attrib: if corner == '': terrain_list.append(None) @@ -312,7 +319,7 @@ def _parse_tiles(tile_element_list: List[etree.Element] terrain_list.append(int(corner)) tile_terrain = objects.TileTerrain(*terrain_list) - # tile element optional sub-elements + #tile element optional sub-elements animation: Optional[List[objects.Frame]] = None tile_animation_element = tile_element.find('./animation') if tile_animation_element: @@ -332,21 +339,19 @@ def _parse_tiles(tile_element_list: List[etree.Element] 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 + 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, type, tile_terrain, animation, - tile_image, object_group) + 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. + """Parse image element given. Returns: :Color: Color in Arcade's preffered format. @@ -361,15 +366,14 @@ def _parse_image_element(image_element: etree.Element) -> objects.Image: width = int(image_element.attrib['width']) height = int(image_element.attrib['height']) - size = objects.OrderedPair(width, height) + size = objects.Size(width, height) return objects.Image(source, size, trans) def _parse_properties_element(properties_element: etree.Element ) -> objects.Properties: - """ - Adds Tiled property to Properties dict. + """Adds Tiled property to Properties dict. Args: :name (str): Name of property. diff --git a/pytiled_parser/utilities.py b/pytiled_parser/utilities.py index a7bf9fc..32d308e 100644 --- a/pytiled_parser/utilities.py +++ b/pytiled_parser/utilities.py @@ -9,7 +9,7 @@ def parse_color(color: str) -> objects.Color: :Color: Color object in the format that Arcade understands. """ # strip initial '#' character - if not len(color) % 2 == 0: # pylint: disable=C2001 + if not len(color) % 2 == 0: color = color[1:] if len(color) == 6: @@ -25,3 +25,21 @@ def parse_color(color: str) -> objects.Color: blue = int(color[6:8], 16) 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 diff --git a/tests/test_data/tile_set_image_hitboxes.tsx b/tests/test_data/tile_set_image_hitboxes.tsx new file mode 100644 index 0000000..6d65375 --- /dev/null +++ b/tests/test_data/tile_set_image_hitboxes.tsx @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/unit2/test_pytiled_parser.py b/tests/unit2/test_pytiled_parser.py index a1044e1..d341f42 100644 --- a/tests/unit2/test_pytiled_parser.py +++ b/tests/unit2/test_pytiled_parser.py @@ -15,7 +15,8 @@ def test_map_simple(): """ 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 # unsure how to get paths to compare propperly From 3c8fcdf8e22bcbc385072ba8cde460222d73085a Mon Sep 17 00:00:00 2001 From: Benjamin Kirkbride Date: Mon, 6 May 2019 19:40:07 -0400 Subject: [PATCH 2/5] fixed bug with last commit --- pytiled_parser/parser.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pytiled_parser/parser.py b/pytiled_parser/parser.py index 5a49f72..79ecf11 100644 --- a/pytiled_parser/parser.py +++ b/pytiled_parser/parser.py @@ -193,7 +193,7 @@ def _parse_layer_type(layer_element: etree.Element) -> objects.LayerType: # return _parse_layer_group(layer_element, layer_type_object) -def _parse_objects(objects: List[etree.Element]) -> List[objects.Object]: +def _parse_objects(object_elements: List[etree.Element]) -> List[objects.Object]: """ """ tiled_objects: List[objects.Object] = [] @@ -216,7 +216,7 @@ def _parse_objects(objects: List[etree.Element]) -> List[objects.Object]: except KeyError: height = 0 - object.size = objects.OrderedPair(width, height) + object.size = objects.Size(width, height) try: object.opacity = round( From e8809745bede85c41a25ddfb53bafb6a6832a4c4 Mon Sep 17 00:00:00 2001 From: Benjamin Kirkbride Date: Mon, 6 May 2019 20:43:22 -0400 Subject: [PATCH 3/5] fixed image bug --- pytiled_parser/objects.py | 72 ++-- pytiled_parser/parser.py | 357 ++++++++++--------- test/output.py | 202 +++++++++-- test/test_tiled.py | 2 +- tests/test_data/test_map_simple_hitboxes.tmx | 23 ++ 5 files changed, 442 insertions(+), 214 deletions(-) create mode 100644 tests/test_data/test_map_simple_hitboxes.tmx diff --git a/pytiled_parser/objects.py b/pytiled_parser/objects.py index 6272a37..f53c617 100644 --- a/pytiled_parser/objects.py +++ b/pytiled_parser/objects.py @@ -35,6 +35,7 @@ class Color(NamedTuple): :blue (int): Blue, between 1 and 255. :alpha (int): Alpha, between 1 and 255. """ + red: int green: int blue: int @@ -48,6 +49,7 @@ class OrderedPair(NamedTuple): x (Union[int, float]): X coordinate. y (Union[int, float]): Y coordinate. """ + x: Union[int, float] y: Union[int, float] @@ -59,6 +61,7 @@ class Size(NamedTuple): 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] @@ -83,13 +86,15 @@ class Chunk: :layer_data (List[List(int)]): The global tile IDs in chunky according to row. """ + location: OrderedPair width: int height: int chunk_data: List[List[int]] -class Image(NamedTuple): +@dataclasses.dataclass +class Image: """ Image object. @@ -104,9 +109,10 @@ class Image(NamedTuple): (optional, used for tile index correction when the image changes). :height (Optional[str]): The image height in pixels (optional). """ + source: str - size: Size - trans: Optional[Color] + size: Optional[Size] = None + trans: Optional[Color] = None Properties = Dict[str, Union[int, float, Color, Path, str]] @@ -120,6 +126,7 @@ class Grid(NamedTuple): determines how tile overlays for terrain and collision information are rendered. """ + orientation: str width: int height: int @@ -134,6 +141,7 @@ class Terrain(NamedTuple): :tile (int): The local tile-id of the tile that represents the terrain visually. """ + name: str tile: int @@ -150,6 +158,7 @@ class Frame(NamedTuple): :duration (int): How long in milliseconds this frame should be displayed before advancing to the next frame. """ + tile_id: int duration: int @@ -168,6 +177,7 @@ class TileTerrain: :bottom_left (Optional[int]): Bottom left terrain type. :bottom_right (Optional[int]): Bottom right terrain type. """ + top_left: Optional[int] = None top_right: Optional[int] = None bottom_left: Optional[int] = None @@ -246,13 +256,13 @@ class Layer(LayerType, _LayerBase): @dataclasses.dataclass -class _ObjectBase: +class _TiledObjectBase: id: int location: OrderedPair @dataclasses.dataclass -class _ObjectDefaults: +class _TiledObjectDefaults: size: Size = Size(0, 0) rotation: int = 0 opacity: int = 0xFF @@ -265,9 +275,9 @@ class _ObjectDefaults: @dataclasses.dataclass -class Object(_ObjectDefaults, _ObjectBase): +class TiledObject(_TiledObjectDefaults, _TiledObjectBase): """ - ObjectGroup Object. + TiledObjectGroup object. See: https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#object @@ -284,14 +294,14 @@ class Object(_ObjectDefaults, _ObjectBase): :opacity (int): The opacity of the object. (default: 255) :name (Optional[str]): The name 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 FIXME """ @dataclasses.dataclass -class RectangleObject(Object): +class RectangleObject(TiledObject): """ Rectangle shape defined by a point, width, and height. @@ -302,7 +312,7 @@ class RectangleObject(Object): @dataclasses.dataclass -class ElipseObject(Object): +class ElipseObject(TiledObject): """ Elipse shape defined by a point, width, and height. @@ -311,7 +321,7 @@ class ElipseObject(Object): @dataclasses.dataclass -class PointObject(Object): +class PointObject(TiledObject): """ Point defined by a point (x,y). @@ -320,12 +330,12 @@ class PointObject(Object): @dataclasses.dataclass -class _TileObjectBase(_ObjectBase): +class _TileImageObjectBase(_TiledObjectBase): gid: int @dataclasses.dataclass -class TileObject(Object, _TileObjectBase): +class TileImageObject(TiledObject, _TileImageObjectBase): """ Polygon shape defined by a set of connections between points. @@ -337,12 +347,12 @@ class TileObject(Object, _TileObjectBase): @dataclasses.dataclass -class _PointsObjectBase(_ObjectBase): +class _PointsObjectBase(_TiledObjectBase): points: List[OrderedPair] @dataclasses.dataclass -class PolygonObject(Object, _PointsObjectBase): +class PolygonObject(TiledObject, _PointsObjectBase): """ Polygon shape defined by a set of connections between points. @@ -354,7 +364,7 @@ class PolygonObject(Object, _PointsObjectBase): @dataclasses.dataclass -class PolylineObject(Object, _PointsObjectBase): +class PolylineObject(TiledObject, _PointsObjectBase): """ Polyline defined by a set of connections between points. @@ -368,13 +378,13 @@ class PolylineObject(Object, _PointsObjectBase): @dataclasses.dataclass -class _TextObjectBase(_ObjectBase): +class _TextObjectBase(_TiledObjectBase): text: str @dataclasses.dataclass -class _TextObjectDefaults(_ObjectDefaults): - font_family: str = 'sans-serif' +class _TextObjectDefaults(_TiledObjectDefaults): + font_family: str = "sans-serif" font_size: int = 16 wrap: bool = False color: Color = Color(0xFF, 0, 0, 0) @@ -383,12 +393,12 @@ class _TextObjectDefaults(_ObjectDefaults): underline: bool = False strike_out: bool = False kerning: bool = False - horizontal_align: str = 'left' - vertical_align: str = 'top' + horizontal_align: str = "left" + vertical_align: str = "top" @dataclasses.dataclass -class TextObject(Object, _TextObjectDefaults, _TextObjectBase): +class TextObject(TiledObject, _TextObjectDefaults, _TextObjectBase): """ Text object with associated settings. @@ -414,19 +424,19 @@ class TextObject(Object, _TextObjectDefaults, _TextObjectBase): @dataclasses.dataclass class _ObjectGroupBase(_LayerTypeBase): - objects: List[Object] + objects: List[TiledObject] @dataclasses.dataclass class _ObjectGroupDefaults(_LayerTypeDefaults): color: Optional[Color] = None - draw_order: Optional[str] = 'topdown' + 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 \ “object layer” in Tiled. @@ -443,7 +453,8 @@ https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#objectgroup to 'topdown'. See: https://doc.mapeditor.org/en/stable/manual/objects/#changing-stacking-order for more info. - :objects (Dict[int, Object]): Dict Object objects by Object.id. + :objects (Dict[int, TiledObject]): Dict TiledObject objects by + TiledObject.id. """ @@ -488,12 +499,13 @@ class Tile: :animation (List[Frame]): Each tile can have exactly one animation associated with it. """ + id: int type: Optional[str] terrain: Optional[TileTerrain] animation: Optional[List[Frame]] image: Optional[Image] - hitboxes: Optional[List[Object]] + hitboxes: Optional[List[TiledObject]] @dataclasses.dataclass @@ -526,6 +538,7 @@ class TileSet: file. :tiles (Optional[Dict[int, Tile]]): Dict of Tile objects by Tile.id. """ + name: str max_tile_size: Size spacing: Optional[int] @@ -581,6 +594,7 @@ class TileMap: is a TileSet object. :layers List[LayerType]: List of layer objects by draw order. """ + parent_dir: Path version: str @@ -604,7 +618,7 @@ class TileMap: properties: Optional[Properties] = None -''' +""" [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:17] <__m4ch1n3__> !py3 [i for i in [1,2,3,4,1,2,3,4] if i < 3] @@ -620,4 +634,4 @@ class TileMap: [22:23] __m4ch1n3__: 100 [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) -''' +""" diff --git a/pytiled_parser/parser.py b/pytiled_parser/parser.py index 79ecf11..356eef4 100644 --- a/pytiled_parser/parser.py +++ b/pytiled_parser/parser.py @@ -60,16 +60,20 @@ def _decode_csv_layer(data_text): lines = lines[:-1] for line in lines: line_list = line.split(",") - while '' in line_list: - line_list.remove('') + 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]]: +def _decode_data( + element: etree.Element, + layer_width: int, + encoding: str, + compression: Optional[str], +) -> List[List[int]]: """Decodes data or chunk data. Args: @@ -81,30 +85,29 @@ def _decode_data(element: etree.Element, layer_width: int, encoding: str, :compression (str): Compression format of the layer data. """ # etree.Element.text comes with an appended and a prepended '\n' - supported_encodings = ['base64', 'csv'] + supported_encodings = ["base64", "csv"] if encoding not in supported_encodings: - raise ValueError('{encoding} is not a valid encoding') + raise ValueError("{encoding} is not a valid encoding") - supported_compression = [None, 'gzip', 'zlib'] + supported_compression = [None, "gzip", "zlib"] if compression is not None: - if encoding != 'base64': - raise ValueError('{encoding} does not support compression') + 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') + raise ValueError("{compression} is not a valid compression type") try: data_text = element.text # type: ignore except AttributeError: - raise AttributeError('{element} lacks layer data.') + raise AttributeError("{element} lacks layer data.") - if encoding == 'csv': + 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: +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. @@ -116,103 +119,109 @@ def _parse_data(element: etree.Element, Returns: :LayerData: Data object containing layer data or chunks of data. """ - encoding = element.attrib['encoding'] + encoding = element.attrib["encoding"] compression = None try: - compression = element.attrib['compression'] + compression = element.attrib["compression"] except KeyError: pass - chunk_elements = element.findall('./chunk') + 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']) + 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) + 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: +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']) + width = int(element.attrib["width"]) + height = int(element.attrib["height"]) size = objects.OrderedPair(width, height) - data_element = element.find('./data') + 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.') + 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']) + id = int(layer_element.attrib["id"]) - name = layer_element.attrib['name'] + name = layer_element.attrib["name"] layer_type_object = objects.LayerType(id, name) try: - offset_x = float(layer_element.attrib['offsetx']) + offset_x = float(layer_element.attrib["offsetx"]) except KeyError: offset_x = 0 try: - offset_y = float(layer_element.attrib['offsety']) + 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) + float(layer_element.attrib["opacity"]) * 255 + ) except KeyError: pass - properties_element = layer_element.find('./properties') + properties_element = layer_element.find("./properties") if properties_element is not None: layer_type_object.properties = _parse_properties_element( - properties_element) + properties_element + ) - if layer_element.tag == 'layer': + if layer_element.tag == "layer": return _parse_layer(layer_element, layer_type_object) - elif layer_element.tag == 'objectgroup': + 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_objects(object_elements: List[etree.Element]) -> List[objects.Object]: +def _parse_objects( + object_elements: List[etree.Element] +) -> List[objects.TiledObject]: """ """ - tiled_objects: List[objects.Object] = [] + 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']) + 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) + object = objects.TiledObject(id, location) try: - width = float(object_element.attrib['width']) + width = float(object_element.attrib["width"]) except KeyError: width = 0 try: - height = float(object_element.attrib['height']) + height = float(object_element.attrib["height"]) except KeyError: height = 0 @@ -220,21 +229,22 @@ def _parse_objects(object_elements: List[etree.Element]) -> List[objects.Object] try: object.opacity = round( - float(object_element.attrib['opacity']) * 255) + float(object_element.attrib["opacity"]) * 255 + ) except KeyError: pass try: - object.rotation = int(object_element.attrib['rotation']) + object.rotation = int(object_element.attrib["rotation"]) except KeyError: pass try: - object.name = object_element.attrib['name'] + object.name = object_element.attrib["name"] except KeyError: pass - properties_element = object_element.find('./properties') + properties_element = object_element.find("./properties") if properties_element is not None: object.properties = _parse_properties_element(properties_element) @@ -243,23 +253,24 @@ def _parse_objects(object_elements: List[etree.Element]) -> List[objects.Object] return tiled_objects -def _parse_object_group(element: etree.Element, - layer_type: objects.LayerType) -> objects.ObjectGroup: +def _parse_object_group( + element: etree.Element, layer_type: objects.LayerType +) -> objects.ObjectGroup: """Parse the objectgroup element given. Args: layer_type (objects.LayerType): """ - tiled_objects = _parse_objects(element.findall('./object')) + tiled_objects = _parse_objects(element.findall("./object")) object_group = objects.ObjectGroup(tiled_objects, **layer_type.__dict__) try: - color = utilities.parse_color(element.attrib['color']) + color = utilities.parse_color(element.attrib["color"]) except KeyError: pass try: - draw_order = element.attrib['draworder'] + draw_order = element.attrib["draworder"] except KeyError: pass @@ -267,85 +278,88 @@ def _parse_object_group(element: etree.Element, @functools.lru_cache() -def _parse_external_tile_set(parent_dir: Path, tile_set_element: etree.Element - ) -> objects.TileSet: +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']) + 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.Object]: - return _parse_objects(element.findall('./object')) +def _parse_hitboxes(element: etree.Element) -> List[objects.TiledObject]: + return _parse_objects(element.findall("./object")) -def _parse_tiles(tile_element_list: List[etree.Element] - ) -> Dict[int, objects.Tile]: +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']) + # id is not optional + id = int(tile_element.attrib["id"]) - #optional attributes + # optional attributes tile_type = None try: - tile_type = tile_element.attrib['type'] + tile_type = tile_element.attrib["type"] except KeyError: pass tile_terrain = None try: - tile_terrain_attrib = tile_element.attrib['terrain'] + tile_terrain_attrib = tile_element.attrib["terrain"] except KeyError: pass else: - #below is an attempt to explain how terrains are handled. + # 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 + # 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 + # 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 + # each index in terrain_list_attrib reffers to a corner for corner in terrain_list_attrib: - if corner == '': + if corner == "": terrain_list.append(None) else: terrain_list.append(int(corner)) tile_terrain = objects.TileTerrain(*terrain_list) - #tile element optional sub-elements + # tile element optional sub-elements animation: Optional[List[objects.Frame]] = None - tile_animation_element = tile_element.find('./animation') + tile_animation_element = tile_element.find("./animation") if tile_animation_element: animation = [] - frames = tile_animation_element.findall('./frame') + 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']) + tile_id = int(frame.attrib["tileid"]) # duration is in MS. Should perhaps be converted to seconds. # FIXME: make decision - duration = int(frame.attrib['duration']) + 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') + 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') + 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) + tiles[id] = objects.Tile( + id, tile_type, tile_terrain, animation, tile_image, hitboxes + ) return tiles @@ -356,23 +370,25 @@ def _parse_image_element(image_element: etree.Element) -> objects.Image: Returns: :Color: Color in Arcade's preffered format. """ - source = image_element.attrib['source'] + 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)) - trans = None try: - trans = utilities.parse_color(image_element.attrib['trans']) + image.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.Size(width, height) - - return objects.Image(source, size, trans) + return image -def _parse_properties_element(properties_element: etree.Element - ) -> objects.Properties: +def _parse_properties_element( + properties_element: etree.Element +) -> objects.Properties: """Adds Tiled property to Properties dict. Args: @@ -385,29 +401,30 @@ def _parse_properties_element(properties_element: etree.Element :Properties: Properties Dict object. """ properties: objects.Properties = {} - for property_element in properties_element.findall('./property'): - name = property_element.attrib['name'] + for property_element in properties_element.findall("./property"): + name = property_element.attrib["name"] try: - property_type = property_element.attrib['type'] + 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_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}") + property_types = ["string", "int", "float", "bool", "color", "file"] + assert ( + property_type in property_types + ), f"Invalid type for property {name}" - if property_type == 'int': + if property_type == "int": properties[name] = int(value) - elif property_type == 'float': + elif property_type == "float": properties[name] = float(value) - elif property_type == 'color': + elif property_type == "color": properties[name] = utilities.parse_color(value) - elif property_type == 'file': + elif property_type == "file": properties[name] = Path(value) - elif property_type == 'bool': - if value == 'true': + elif property_type == "bool": + if value == "true": properties[name] = True else: properties[name] = False @@ -422,75 +439,86 @@ 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']) + 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']) + spacing = int(tile_set_element.attrib["spacing"]) except KeyError: pass margin = None try: - margin = int(tile_set_element.attrib['margin']) + margin = int(tile_set_element.attrib["margin"]) except KeyError: pass tile_count = None try: - tile_count = int(tile_set_element.attrib['tilecount']) + tile_count = int(tile_set_element.attrib["tilecount"]) except KeyError: pass columns = None try: - columns = int(tile_set_element.attrib['columns']) + columns = int(tile_set_element.attrib["columns"]) except KeyError: pass tile_offset = None - tileoffset_element = tile_set_element.find('./tileoffset') + 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_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') + 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_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') + 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') + 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']) + 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') + 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') + 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) + 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]): @@ -501,26 +529,26 @@ def parse_tile_map(tmx_file: Union[str, Path]): # 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']) + 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_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 + 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']) + 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') + 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 @@ -529,56 +557,67 @@ def parse_tile_map(tmx_file: Union[str, Path]): # 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']) + firstgid = int(tile_set_element.attrib["firstgid"]) try: # check if is an external TSX - source = tile_set_element.attrib['source'] + source = tile_set_element.attrib["source"] except KeyError: # the tile set in embedded - name = tile_set_element.attrib['name'] + 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) + parent_dir, tile_set_element + ) # parse all layers layers: List[objects.LayerType] = [] - layer_tags = ['layer', 'objectgroup', 'group'] - for element in map_element.findall('./'): + 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) + 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']) + tile_map.hex_side_length = int(map_element.attrib["hexsidelength"]) except KeyError: pass try: - tile_map.stagger_axis = int(map_element.attrib['staggeraxis']) + tile_map.stagger_axis = int(map_element.attrib["staggeraxis"]) except KeyError: pass try: - tile_map.stagger_index = int(map_element.attrib['staggerindex']) + tile_map.stagger_index = int(map_element.attrib["staggerindex"]) except KeyError: pass try: - backgroundcolor = map_element.attrib['backgroundcolor'] + backgroundcolor = map_element.attrib["backgroundcolor"] except KeyError: pass else: tile_map.background_color = utilities.parse_color(backgroundcolor) - properties_element = map_tree.find('./properties') + properties_element = map_tree.find("./properties") if properties_element is not None: tile_map.properties = _parse_properties_element(properties_element) diff --git a/test/output.py b/test/output.py index 2b5305f..4faf5ab 100644 --- a/test/output.py +++ b/test/output.py @@ -1,25 +1,177 @@ -{ 'background_color': None, - 'height': 6, - 'hex_side_length': None, - '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)], - 'next_layer_id': 2, - 'next_object_id': 1, - 'orientation': 'orthogonal', - 'parent_dir': PosixPath('/home/ben/Projects/pytiled_parser/pytiled_parser-venv/pytiled_parser/tests/test_data'), - 'properties': { 'bool property - false': False, - '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', - 'stagger_axis': None, - 'stagger_index': None, - 'tile_height': 32, - '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_width': 32, - 'tiled_version': '1.2.3', - 'version': '1.2', - 'width': 8} +{ + "background_color": None, + "hex_side_length": None, + "infinite": False, + "layers": [ + Layer( + size=OrderedPair(x=8, y=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, + ) + ], + "map_size": OrderedPair(x=8, y=6), + "next_layer_id": 2, + "next_object_id": 1, + "orientation": "orthogonal", + "parent_dir": PosixPath( + "/home/benk/Projects/pytiled_parser/venv/pytiled_parser/tests/test_data" + ), + "properties": { + "bool property - false": False, + "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", + "stagger_axis": None, + "stagger_index": None, + "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", + size=Size(width=265, height=199), + trans=None, + ), + terrain_types=None, + tiles={ + 9: Tile( + id=9, + type=None, + terrain=None, + animation=None, + image=None, + hitboxes=[ + Object( + id=2, + location=OrderedPair(x=1.0, y=1.0), + size=Size(width=32.0, height=32.0), + rotation=1, + opacity=255, + name="wall", + type=None, + properties=None, + template=None, + ) + ], + ), + 19: Tile( + id=19, + type=None, + terrain=None, + animation=None, + image=None, + hitboxes=[ + Object( + id=1, + location=OrderedPair(x=32.0, y=1.0), + size=Size(width=0, height=0), + rotation=1, + opacity=255, + name="wall corner", + type=None, + properties=None, + template=None, + ) + ], + ), + 20: Tile( + id=20, + type=None, + terrain=None, + animation=None, + image=None, + hitboxes=[ + Object( + id=1, + location=OrderedPair(x=1.45455, y=1.45455), + size=Size(width=0, height=0), + rotation=1, + opacity=255, + name="polyline", + type=None, + properties=None, + template=None, + ) + ], + ), + 31: Tile( + id=31, + type=None, + terrain=None, + animation=None, + image=None, + hitboxes=[ + Object( + id=1, + location=OrderedPair(x=5.09091, y=2.54545), + size=Size(width=19.6364, height=19.2727), + rotation=1, + opacity=255, + name="rock 1", + type=None, + properties=None, + template=None, + ), + Object( + id=2, + location=OrderedPair(x=16.1818, y=22.0), + size=Size(width=8.54545, height=8.36364), + rotation=-1, + opacity=255, + name="rock 2", + type=None, + properties=None, + template=None, + ), + ], + ), + 45: Tile( + id=45, + type=None, + terrain=None, + animation=None, + image=None, + hitboxes=[ + Object( + id=1, + location=OrderedPair(x=14.7273, y=26.3636), + size=Size(width=0, height=0), + rotation=0, + opacity=255, + name="sign", + type=None, + properties=None, + template=None, + ) + ], + ), + }, + ) + }, + "tile_size": OrderedPair(x=32, y=32), + "tiled_version": "1.2.3", + "version": "1.2", +} diff --git a/test/test_tiled.py b/test/test_tiled.py index 431e072..3a6dde2 100644 --- a/test/test_tiled.py +++ b/test/test_tiled.py @@ -10,7 +10,7 @@ pp = pprint.PrettyPrinter(indent=4, compact=True, width=100) 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_simple_hitboxes.tmx' map = pytiled_parser.parse_tile_map(MAP_NAME) diff --git a/tests/test_data/test_map_simple_hitboxes.tmx b/tests/test_data/test_map_simple_hitboxes.tmx new file mode 100644 index 0000000..f197ed6 --- /dev/null +++ b/tests/test_data/test_map_simple_hitboxes.tmx @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + +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 + + + From 69363f22fa14b7c7bab6f4e56fb8d7d65137c240 Mon Sep 17 00:00:00 2001 From: Benjamin Kirkbride Date: Wed, 8 May 2019 15:21:10 -0400 Subject: [PATCH 4/5] yay --- pytiled_parser/__init__.py | 2 +- pytiled_parser/objects.py | 147 ++++------ pytiled_parser/{parser.py => xml_parser.py} | 274 +++++++++++++----- setup.py | 58 ++-- test/output.py | 197 ++----------- test/test_tiled.py | 2 +- tests/unit2/test_parser.py | 18 ++ ...=> test_pytiled_parser_integration.py.bax} | 53 ++-- 8 files changed, 350 insertions(+), 401 deletions(-) rename pytiled_parser/{parser.py => xml_parser.py} (73%) create mode 100644 tests/unit2/test_parser.py rename tests/unit2/{test_pytiled_parser.py => test_pytiled_parser_integration.py.bax} (66%) diff --git a/pytiled_parser/__init__.py b/pytiled_parser/__init__.py index 5ec4574..bcfa056 100644 --- a/pytiled_parser/__init__.py +++ b/pytiled_parser/__init__.py @@ -1,4 +1,4 @@ from . import utilities from . import objects -from .parser import parse_tile_map +from .xml_parser import parse_tile_map diff --git a/pytiled_parser/objects.py b/pytiled_parser/objects.py index f53c617..49a15e6 100644 --- a/pytiled_parser/objects.py +++ b/pytiled_parser/objects.py @@ -14,18 +14,6 @@ import xml.etree.ElementTree as etree 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): """Color object. @@ -185,44 +173,35 @@ class TileTerrain: @dataclasses.dataclass -class _LayerTypeBase: - id: int # pylint: disable=C0103 - 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. +class Layer: + """Class that all layers inherret from. Args: - :layer_element (etree.Element): Element to be parsed into a - LayerType object. - - Attributes: - :id (int): Unique ID of the layer. Each layer that added to a map - gets a unique id. Even if a layer is deleted, no layer ever gets - the same ID. - :name (Optional[str):] The name of the layer object. - :offset (OrderedPair): Rendering offset of the layer object in - pixels. (default: (0, 0). - :opacity (int): Value between 0 and 255 to determine opacity. NOTE: - this value is converted from a float provided by Tiled, so some - precision is lost. - :properties (Optional[Properties]): Properties object for layer - object. + id: Unique ID of the layer. Each layer that added to a map gets a + unique id. Even if a layer is deleted, no layer ever gets the same + ID. + name: The name of the layer object. + tiled_objects: List of tiled_objects in the layer. + offset: Rendering offset of the layer object in pixels. + opacity: Decimal value between 0 and 1 to determine opacity. 1 is + completely opaque, 0 is completely transparent. + properties: Properties for the layer. + color: The color used to display the objects in this group. + FIXME: editor only? + draworder: Whether the objects are drawn according to the order of the + 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 + for more info. """ + id: int + name: str + + offset: Optional[OrderedPair] + opacity: Optional[float] + properties: Optional[Properties] + LayerData = Union[List[List[int]], List[Chunk]] """ @@ -234,26 +213,22 @@ Either a 2 dimensional array of integers representing the global tile IDs @dataclasses.dataclass -class _LayerBase: - size: Size - data: LayerData - - -@dataclasses.dataclass -class Layer(LayerType, _LayerBase): - """ - Map layer object. +class TileLayer(Layer): + """Tile map layer containing tiles. See: https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#layer - Attributes: - :size (Size): 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. + 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 + @dataclasses.dataclass class _TiledObjectBase: @@ -265,7 +240,7 @@ class _TiledObjectBase: class _TiledObjectDefaults: size: Size = Size(0, 0) rotation: int = 0 - opacity: int = 0xFF + opacity: float = 1 name: Optional[str] = None type: Optional[str] = None @@ -277,7 +252,7 @@ class _TiledObjectDefaults: @dataclasses.dataclass class TiledObject(_TiledObjectDefaults, _TiledObjectBase): """ - TiledObjectGroup object. + TiledObject object. See: https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#object @@ -423,48 +398,36 @@ class TextObject(TiledObject, _TextObjectDefaults, _TextObjectBase): @dataclasses.dataclass -class _ObjectGroupBase(_LayerTypeBase): - objects: List[TiledObject] - - -@dataclasses.dataclass -class _ObjectGroupDefaults(_LayerTypeDefaults): - color: Optional[Color] = None - draw_order: Optional[str] = "topdown" - - -@dataclasses.dataclass -class ObjectGroup(LayerType, _ObjectGroupDefaults, _ObjectGroupBase): +class ObjectLayer(Layer): """ TiledObject Group Object. The object group is in fact a map layer, and is hence called \ “object layer” in Tiled. - See: \ -https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#objectgroup + See: + https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#objectgroup - Attributes: - :color (Optional[Color]): The color used to display the objects - in this group. FIXME: editor only? - :draworder (str): Whether the objects are drawn according to the - order of the object elements in the object group element - ('manual'), or sorted by their y-coordinate ('topdown'). Defaults - to 'topdown'. See: + Args: + tiled_objects: List of tiled_objects in the layer. + offset: Rendering offset of the layer object in pixels. + color: The color used to display the objects in this group. + FIXME: editor only? + draworder: Whether the objects are drawn according to the order of the + 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 for more info. - :objects (Dict[int, TiledObject]): Dict TiledObject objects by - TiledObject.id. """ + tiled_objects: List[TiledObject] -@dataclasses.dataclass -class _LayerGroupBase(_LayerTypeBase): - layers: Optional[List[LayerType]] + color: Optional[Color] = None + draw_order: Optional[str] = "topdown" @dataclasses.dataclass -class LayerGroup(LayerType): +class LayerGroup(Layer): """ Layer Group. @@ -479,6 +442,8 @@ class LayerGroup(LayerType): """ + layers: Optional[List[Union["LayerGroup", Layer, ObjectLayer]]] + @dataclasses.dataclass class Hitbox: @@ -608,7 +573,7 @@ class TileMap: next_object_id: int tile_sets: TileSetDict - layers: List[LayerType] + layers: List[Layer] hex_side_length: Optional[int] = None stagger_axis: Optional[int] = None diff --git a/pytiled_parser/parser.py b/pytiled_parser/xml_parser.py similarity index 73% rename from pytiled_parser/parser.py rename to pytiled_parser/xml_parser.py index 356eef4..4f7c19d 100644 --- a/pytiled_parser/parser.py +++ b/pytiled_parser/xml_parser.py @@ -6,15 +6,16 @@ import zlib from pathlib import Path -from typing import Dict, List, Optional, Union +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 -import xml.etree.ElementTree as etree - -def _decode_base64_data(data_text, compression, layer_width): +def _decode_base64_data( + data_text: str, compression: Optional[str], layer_width: int +) -> List[List[int]]: tile_grid: List[List[int]] = [[]] unencoded_data = base64.b64decode(data_text) @@ -48,13 +49,13 @@ def _decode_base64_data(data_text, compression, layer_width): return tile_grid -def _decode_csv_layer(data_text): +def _decode_csv_layer(data_text: str) -> List[List[int]]: """Decodes csv encoded layer data. Credit: """ tile_grid = [] - lines = data_text.split("\n") + lines: List[str] = data_text.split("\n") # remove erronious empty lists due to a newline being on both ends of text lines = lines[1:] lines = lines[:-1] @@ -97,9 +98,9 @@ def _decode_data( raise ValueError("{compression} is not a valid compression type") try: - data_text = element.text # type: ignore + data_text: str = element.text # type: ignore except AttributeError: - raise AttributeError("{element} lacks layer data.") + raise AttributeError(f"{element} lacks layer data.") if encoding == "csv": return _decode_csv_layer(data_text) @@ -107,7 +108,9 @@ def _decode_data( return _decode_base64_data(data_text, compression, layer_width) -def _parse_data(element: etree.Element, layer_width: int) -> objects.LayerData: +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. @@ -145,65 +148,98 @@ def _parse_data(element: etree.Element, layer_width: int) -> objects.LayerData: 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.") + layer_element: etree.Element +) -> Tuple[ + int, + str, + Optional[objects.OrderedPair], + Optional[float], + Optional[objects.Properties], +]: + """Parses all of the attributes for a Layer object. - return objects.Layer(size, data, **layer_type.__dict__) + Args: + layer_element: The layer element to be parsed. + Returns: -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) + 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 - try: - offset_x = float(layer_element.attrib["offsetx"]) - except KeyError: - offset_x = 0 + offset = objects.OrderedPair(offset_x, offset_y) + else: + offset = None - 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 + 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: - layer_type_object.properties = _parse_properties_element( - properties_element - ) + properties = _parse_properties_element(properties_element) + else: + properties = None - 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) + 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] = [] @@ -213,7 +249,7 @@ def _parse_objects( location_y = float(object_element.attrib["y"]) location = objects.OrderedPair(location_x, location_y) - object = objects.TiledObject(id, location) + tiled_object = objects.TiledObject(id, location) try: width = float(object_element.attrib["width"]) @@ -225,45 +261,57 @@ def _parse_objects( except KeyError: height = 0 - object.size = objects.Size(width, height) + tiled_object.size = objects.Size(width, height) try: - object.opacity = round( - float(object_element.attrib["opacity"]) * 255 - ) + tiled_object.opacity = float(object_element.attrib["opacity"]) except KeyError: pass try: - object.rotation = int(object_element.attrib["rotation"]) + tiled_object.rotation = int(object_element.attrib["rotation"]) except KeyError: pass try: - object.name = object_element.attrib["name"] + 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: - object.properties = _parse_properties_element(properties_element) + tiled_object.properties = _parse_properties_element( + properties_element + ) - tiled_objects.append(object) + tiled_objects.append(tiled_object) return tiled_objects -def _parse_object_group( - element: etree.Element, layer_type: objects.LayerType -) -> objects.ObjectGroup: +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")) - object_group = objects.ObjectGroup(tiled_objects, **layer_type.__dict__) try: color = utilities.parse_color(element.attrib["color"]) except KeyError: @@ -274,7 +322,85 @@ def _parse_object_group( except KeyError: pass - return object_group + 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() @@ -283,7 +409,7 @@ def _parse_external_tile_set( ) -> objects.TileSet: """Parses an external tile set. - Caches the results to speed up subsequent instances. + 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() @@ -292,6 +418,7 @@ def _parse_external_tile_set( def _parse_hitboxes(element: etree.Element) -> List[objects.TiledObject]: + """Parses all hitboxes for a given tile.""" return _parse_objects(element.findall("./object")) @@ -442,7 +569,7 @@ def _parse_tile_set(tile_set_element: etree.Element) -> objects.TileSet: 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) + max_tile_size = objects.Size(max_tile_width, max_tile_height) spacing = None try: @@ -521,7 +648,7 @@ def _parse_tile_set(tile_set_element: etree.Element) -> objects.TileSet: ) -def parse_tile_map(tmx_file: Union[str, Path]): +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() @@ -535,10 +662,10 @@ def parse_tile_map(tmx_file: Union[str, Path]): 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) + 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.OrderedPair(tile_width, tile_height) + tile_size = objects.Size(tile_width, tile_height) infinite_attribute = map_element.attrib["infinite"] infinite = True if infinite_attribute == "true" else False @@ -571,14 +698,7 @@ def parse_tile_map(tmx_file: Union[str, Path]): 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)) + layers = _get_layers(map_element) tile_map = objects.TileMap( parent_dir, diff --git a/setup.py b/setup.py index edb257d..d8b13db 100644 --- a/setup.py +++ b/setup.py @@ -3,38 +3,36 @@ import sys from setuptools import setup BUILD = 0 -VERSION = '0.1' +VERSION = "0.0.1" RELEASE = VERSION -if __name__ == '__main__': - readme = path.join(path.dirname(path.abspath(__file__)), 'README.md') - with open(readme, 'r') as f: +if __name__ == "__main__": + readme = path.join(path.dirname(path.abspath(__file__)), "README.md") + with open(readme, "r") as f: long_desc = f.read() setup( - name='pytiled_parser', - version=RELEASE, - description='Python Library for parsing Tiled Map Editor maps.', - long_description=long_desc, - author='Benjamin Kirkbride', - author_email='BenjaminKirkbride@gmail.com', - license='MIT', - url='https://github.com/Beefy-Swain/pytiled_parser', - download_url='https://github.com/Beefy-Swain/pytiled_parser', - install_requires=[ - 'dataclasses', - ], - packages=['pytiled_parser'], - classifiers=[ - 'Development Status :: 1 - Planning', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: MIT License', - 'Operating System :: OS Independent', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: Implementation :: CPython', - 'Topic :: Software Development :: Libraries :: Python Modules', - ], - test_suite='tests', - ) + name="pytiled_parser", + version=RELEASE, + description="Python Library for parsing Tiled Map Editor maps.", + long_description=long_desc, + author="Benjamin Kirkbride", + author_email="BenjaminKirkbride@gmail.com", + license="MIT", + url="https://github.com/Beefy-Swain/pytiled_parser", + download_url="https://github.com/Beefy-Swain/pytiled_parser", + install_requires=["dataclasses"], + packages=["pytiled_parser"], + classifiers=[ + "Development Status :: 1 - Planning", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: Implementation :: CPython", + "Topic :: Software Development :: Libraries :: Python Modules", + ], + test_suite="tests", + ) diff --git a/test/output.py b/test/output.py index 4faf5ab..7190a30 100644 --- a/test/output.py +++ b/test/output.py @@ -1,177 +1,20 @@ -{ - "background_color": None, - "hex_side_length": None, - "infinite": False, - "layers": [ - Layer( - size=OrderedPair(x=8, y=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, - ) - ], - "map_size": OrderedPair(x=8, y=6), - "next_layer_id": 2, - "next_object_id": 1, - "orientation": "orthogonal", - "parent_dir": PosixPath( - "/home/benk/Projects/pytiled_parser/venv/pytiled_parser/tests/test_data" - ), - "properties": { - "bool property - false": False, - "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", - "stagger_axis": None, - "stagger_index": None, - "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", - size=Size(width=265, height=199), - trans=None, - ), - terrain_types=None, - tiles={ - 9: Tile( - id=9, - type=None, - terrain=None, - animation=None, - image=None, - hitboxes=[ - Object( - id=2, - location=OrderedPair(x=1.0, y=1.0), - size=Size(width=32.0, height=32.0), - rotation=1, - opacity=255, - name="wall", - type=None, - properties=None, - template=None, - ) - ], - ), - 19: Tile( - id=19, - type=None, - terrain=None, - animation=None, - image=None, - hitboxes=[ - Object( - id=1, - location=OrderedPair(x=32.0, y=1.0), - size=Size(width=0, height=0), - rotation=1, - opacity=255, - name="wall corner", - type=None, - properties=None, - template=None, - ) - ], - ), - 20: Tile( - id=20, - type=None, - terrain=None, - animation=None, - image=None, - hitboxes=[ - Object( - id=1, - location=OrderedPair(x=1.45455, y=1.45455), - size=Size(width=0, height=0), - rotation=1, - opacity=255, - name="polyline", - type=None, - properties=None, - template=None, - ) - ], - ), - 31: Tile( - id=31, - type=None, - terrain=None, - animation=None, - image=None, - hitboxes=[ - Object( - id=1, - location=OrderedPair(x=5.09091, y=2.54545), - size=Size(width=19.6364, height=19.2727), - rotation=1, - opacity=255, - name="rock 1", - type=None, - properties=None, - template=None, - ), - Object( - id=2, - location=OrderedPair(x=16.1818, y=22.0), - size=Size(width=8.54545, height=8.36364), - rotation=-1, - opacity=255, - name="rock 2", - type=None, - properties=None, - template=None, - ), - ], - ), - 45: Tile( - id=45, - type=None, - terrain=None, - animation=None, - image=None, - hitboxes=[ - Object( - id=1, - location=OrderedPair(x=14.7273, y=26.3636), - size=Size(width=0, height=0), - rotation=0, - opacity=255, - name="sign", - type=None, - properties=None, - template=None, - ) - ], - ), - }, - ) - }, - "tile_size": OrderedPair(x=32, y=32), - "tiled_version": "1.2.3", - "version": "1.2", -} +{ 'background_color': None, + 'hex_side_length': None, + 'infinite': False, + '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]]), + 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]]), + 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', + 'parent_dir': PosixPath('/home/benk/Projects/pytiled_parser/venv/pytiled_parser/tests/test_data'), + 'properties': None, + 'render_order': 'right-down', + 'stagger_axis': None, + 'stagger_index': None, + '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_size': Size(width=32, height=32), + 'tiled_version': '1.2.3', + 'version': '1.2'} diff --git a/test/test_tiled.py b/test/test_tiled.py index 3a6dde2..235d112 100644 --- a/test/test_tiled.py +++ b/test/test_tiled.py @@ -10,7 +10,7 @@ pp = pprint.PrettyPrinter(indent=4, compact=True, width=100) pp = pp.pprint -MAP_NAME = '/home/benk/Projects/pytiled_parser/venv/pytiled_parser/tests/test_data/test_map_simple_hitboxes.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) diff --git a/tests/unit2/test_parser.py b/tests/unit2/test_parser.py new file mode 100644 index 0000000..a03f8bd --- /dev/null +++ b/tests/unit2/test_parser.py @@ -0,0 +1,18 @@ +import pytest + +import xml.etree.ElementTree as etree + +from typing import Callable + +from pytiled_parser import xml_parser + + +def _get_root_element(xml: str) -> Callable: + pass + + +layer_data = [] + + +def test_parse_layer(element, expected): + pass diff --git a/tests/unit2/test_pytiled_parser.py b/tests/unit2/test_pytiled_parser_integration.py.bax similarity index 66% rename from tests/unit2/test_pytiled_parser.py rename to tests/unit2/test_pytiled_parser_integration.py.bax index d341f42..904548a 100644 --- a/tests/unit2/test_pytiled_parser.py +++ b/tests/unit2/test_pytiled_parser_integration.py.bax @@ -16,7 +16,8 @@ def test_map_simple(): TMX with a very simple spritesheet tile set and some properties. """ map = pytiled_parser.parse_tile_map( - Path("../test_data/test_map_simple.tmx")) + Path("../test_data/test_map_simple.tmx") + ) # map # unsure how to get paths to compare propperly @@ -38,13 +39,13 @@ def test_map_simple(): assert map.background_color == None assert map.properties == { - "bool property - false": False, - "bool property - true": True, - "color property": (0x49, 0xfc, 0xff, 0xff), - "file property": Path("/var/log/syslog"), - "float property": 1.23456789, - "int property": 13, - "string property": "Hello, World!!" + "bool property - false": False, + "bool property - true": True, + "color property": (0x49, 0xFC, 0xFF, 0xFF), + "file property": Path("/var/log/syslog"), + "float property": 1.23456789, + "int property": 13, + "string property": "Hello, World!!", } # tileset @@ -60,7 +61,8 @@ def test_map_simple(): # unsure how to get paths to compare propperly 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.size == (265, 199) @@ -68,28 +70,31 @@ def test_map_simple(): assert map.tile_sets[1].tiles == {} # layers - assert map.layers[0].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]] + assert map.layers[0].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], + ] assert map.layers[0].id == 1 assert map.layers[0].name == "Tile Layer 1" - assert map.layers[0].offset == (0, 0) - assert map.layers[0].opacity == 0xFF + assert map.layers[0].offset == None + assert map.layers[0].opacity == None assert map.layers[0].properties == None assert map.layers[0].size == (8, 6) @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)), - ] + "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): """ From a44c3523d4f2aa6c09a5c669cef13494d6c75e1b Mon Sep 17 00:00:00 2001 From: Benjamin Kirkbride Date: Wed, 8 May 2019 21:05:57 -0400 Subject: [PATCH 5/5] added a lot of tests yay --- pytiled_parser/objects.py | 6 +- pytiled_parser/xml_parser.py | 24 ++-- tests/test_data/test_map_simple_meme.tmx | 18 +++ tests/unit2/test_parser.py | 166 ++++++++++++++++++++++- 4 files changed, 191 insertions(+), 23 deletions(-) create mode 100644 tests/test_data/test_map_simple_meme.tmx diff --git a/pytiled_parser/objects.py b/pytiled_parser/objects.py index 49a15e6..a805a7b 100644 --- a/pytiled_parser/objects.py +++ b/pytiled_parser/objects.py @@ -362,7 +362,7 @@ class _TextObjectDefaults(_TiledObjectDefaults): font_family: str = "sans-serif" font_size: int = 16 wrap: bool = False - color: Color = Color(0xFF, 0, 0, 0) + color: str = "#000000" bold: bool = False italic: bool = False underline: bool = False @@ -422,7 +422,7 @@ class ObjectLayer(Layer): tiled_objects: List[TiledObject] - color: Optional[Color] = None + color: Optional[str] = None draw_order: Optional[str] = "topdown" @@ -578,7 +578,7 @@ class TileMap: hex_side_length: Optional[int] = None stagger_axis: Optional[int] = None stagger_index: Optional[int] = None - background_color: Optional[Color] = None + background_color: Optional[str] = None properties: Optional[Properties] = None diff --git a/pytiled_parser/xml_parser.py b/pytiled_parser/xml_parser.py index 4f7c19d..8ca4649 100644 --- a/pytiled_parser/xml_parser.py +++ b/pytiled_parser/xml_parser.py @@ -14,7 +14,7 @@ import pytiled_parser.utilities as utilities def _decode_base64_data( - data_text: str, compression: Optional[str], layer_width: int + data_text: str, layer_width: int, compression: Optional[str] = None ) -> List[List[int]]: tile_grid: List[List[int]] = [[]] @@ -49,7 +49,7 @@ def _decode_base64_data( return tile_grid -def _decode_csv_layer(data_text: str) -> List[List[int]]: +def _decode_csv_data(data_text: str) -> List[List[int]]: """Decodes csv encoded layer data. Credit: @@ -57,10 +57,10 @@ def _decode_csv_layer(data_text: str) -> List[List[int]]: 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:] - lines = lines[:-1] + 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] @@ -103,7 +103,7 @@ def _decode_data( raise AttributeError(f"{element} lacks layer data.") if encoding == "csv": - return _decode_csv_layer(data_text) + return _decode_csv_data(data_text) return _decode_base64_data(data_text, compression, layer_width) @@ -162,7 +162,7 @@ def _parse_layer( layer_element: The layer element to be parsed. Returns: - + FIXME """ id = int(layer_element.attrib["id"]) @@ -313,7 +313,7 @@ def _parse_object_layer(element: etree.Element,) -> objects.ObjectLayer: tiled_objects = _parse_objects(element.findall("./object")) try: - color = utilities.parse_color(element.attrib["color"]) + color = element.attrib["color"] except KeyError: pass @@ -495,7 +495,7 @@ def _parse_image_element(image_element: etree.Element) -> objects.Image: """Parse image element given. Returns: - :Color: Color in Arcade's preffered format. + : Color in Arcade's preffered format. """ image = objects.Image(image_element.attrib["source"]) @@ -506,7 +506,7 @@ def _parse_image_element(image_element: etree.Element) -> objects.Image: image.size = objects.Size(int(width_attrib), int(height_attrib)) try: - image.trans = utilities.parse_color(image_element.attrib["trans"]) + image.trans = image_element.attrib["trans"] except KeyError: pass @@ -547,7 +547,7 @@ def _parse_properties_element( elif property_type == "float": properties[name] = float(value) elif property_type == "color": - properties[name] = utilities.parse_color(value) + properties[name] = value elif property_type == "file": properties[name] = Path(value) elif property_type == "bool": @@ -731,11 +731,9 @@ def parse_tile_map(tmx_file: Union[str, Path]) -> objects.TileMap: pass try: - backgroundcolor = map_element.attrib["backgroundcolor"] + tile_map.background_color = 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: diff --git a/tests/test_data/test_map_simple_meme.tmx b/tests/test_data/test_map_simple_meme.tmx new file mode 100644 index 0000000..bc47bdc --- /dev/null +++ b/tests/test_data/test_map_simple_meme.tmx @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + H4sIAAAAAAAAAw3DBRKCQAAAwDMRA7BQLMTE9v+vY3dmWyGEth279uwbOTB26MixExNTM6fOnLtwae7KtYUbt+7ce7D0aOXJsxev3rxb+/Dpy7cfv/782wAcvDirwAAAAA== + + + diff --git a/tests/unit2/test_parser.py b/tests/unit2/test_parser.py index a03f8bd..3d58ab1 100644 --- a/tests/unit2/test_parser.py +++ b/tests/unit2/test_parser.py @@ -2,17 +2,169 @@ import pytest import xml.etree.ElementTree as etree -from typing import Callable +from contextlib import contextmanager +from typing import Callable, List, Optional, Tuple -from pytiled_parser import xml_parser +from pytiled_parser import objects, xml_parser, utilities -def _get_root_element(xml: str) -> Callable: - pass +@contextmanager +def does_not_raise(): + yield -layer_data = [] +def _get_root_element(xml: str) -> etree.Element: + return etree.fromstring(xml) -def test_parse_layer(element, expected): - pass +layer_data = [ + ( + '' + "", + (int(1), "Tile Layer 1", None, None, None), + ), + ( + '' + "", + (int(2), "Tile Layer 2", None, float(0.5), None), + ), + ( + '' + "" + "" + "", + ( + 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 + )