From b6dd3c9510e771110ee81fe603155d17ad82a7cf Mon Sep 17 00:00:00 2001 From: Benjamin Kirkbride Date: Mon, 6 May 2019 18:35:45 -0400 Subject: [PATCH] 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