From 45e0784d3d984d6dcf9650163110d4606db3c07a Mon Sep 17 00:00:00 2001 From: Darren Eberly Date: Sun, 7 Nov 2021 02:43:32 -0500 Subject: [PATCH 01/13] Initial 2.0 refactor --- pytiled_parser/__init__.py | 3 +- pytiled_parser/layer.py | 363 +------------------ pytiled_parser/parser.py | 17 + pytiled_parser/parsers/json/layer.py | 364 ++++++++++++++++++++ pytiled_parser/parsers/json/properties.py | 48 +++ pytiled_parser/parsers/json/tiled_map.py | 153 ++++++++ pytiled_parser/parsers/json/tiled_object.py | 321 +++++++++++++++++ pytiled_parser/parsers/json/tileset.py | 272 +++++++++++++++ pytiled_parser/parsers/json/wang_set.py | 104 ++++++ pytiled_parser/properties.py | 51 +-- pytiled_parser/tiled_map.py | 160 +-------- pytiled_parser/tiled_object.py | 302 +--------------- pytiled_parser/tileset.py | 265 +------------- pytiled_parser/version.py | 2 +- pytiled_parser/wang_set.py | 106 +----- pytiled_parser/world.py | 11 +- setup.cfg | 6 +- tests/test_layer.py | 4 +- tests/test_map.py | 4 +- tests/test_tiled_object.py | 82 +++-- tests/test_tileset.py | 4 +- 21 files changed, 1370 insertions(+), 1272 deletions(-) create mode 100644 pytiled_parser/parser.py create mode 100644 pytiled_parser/parsers/json/layer.py create mode 100644 pytiled_parser/parsers/json/properties.py create mode 100644 pytiled_parser/parsers/json/tiled_map.py create mode 100644 pytiled_parser/parsers/json/tiled_object.py create mode 100644 pytiled_parser/parsers/json/tileset.py create mode 100644 pytiled_parser/parsers/json/wang_set.py diff --git a/pytiled_parser/__init__.py b/pytiled_parser/__init__.py index 46255ba6..a58b02f3 100644 --- a/pytiled_parser/__init__.py +++ b/pytiled_parser/__init__.py @@ -13,7 +13,8 @@ from .common_types import OrderedPair, Size from .layer import ImageLayer, Layer, LayerGroup, ObjectLayer, TileLayer +from .parser import parse_map from .properties import Properties -from .tiled_map import TiledMap, parse_map +from .tiled_map import TiledMap from .tileset import Tile, Tileset from .version import __version__ diff --git a/pytiled_parser/layer.py b/pytiled_parser/layer.py index a0237b2a..8a3bc9b5 100644 --- a/pytiled_parser/layer.py +++ b/pytiled_parser/layer.py @@ -8,27 +8,14 @@ # pylint: disable=too-few-public-methods -import base64 -import gzip -import importlib.util -import zlib from pathlib import Path -from typing import Any, List, Optional, Union -from typing import cast as type_cast +from typing import List, Optional, Union import attr -from typing_extensions import TypedDict -from . import properties as properties_ -from . import tiled_object -from .common_types import Color, OrderedPair, Size -from .util import parse_color - -zstd_spec = importlib.util.find_spec("zstd") -if zstd_spec: - import zstd # pylint: disable=import-outside-toplevel -else: - zstd = None # pylint: disable=invalid-name +from pytiled_parser.common_types import Color, OrderedPair, Size +from pytiled_parser.properties import Properties +from pytiled_parser.tiled_object import TiledObject @attr.s(auto_attribs=True, kw_only=True) @@ -60,7 +47,7 @@ class Layer: id: Optional[int] = None size: Optional[Size] = None - properties: Optional[properties_.Properties] = None + properties: Optional[Properties] = None tint_color: Optional[Color] = None @@ -127,7 +114,7 @@ class ObjectLayer(Layer): for more info. """ - tiled_objects: List[tiled_object.TiledObject] + tiled_objects: List[TiledObject] draw_order: Optional[str] = "topdown" @@ -162,341 +149,3 @@ class LayerGroup(Layer): """ layers: Optional[List[Layer]] - - -class RawChunk(TypedDict): - """The keys and their types that appear in a Chunk JSON Object. - - See: https://doc.mapeditor.org/en/stable/reference/json-map-format/#chunk - """ - - data: Union[List[int], str] - height: int - width: int - x: int - y: int - - -class RawLayer(TypedDict): - """The keys and their types that appear in a Layer JSON Object. - - See: https://doc.mapeditor.org/en/stable/reference/json-map-format/#layer - """ - - chunks: List[RawChunk] - compression: str - data: Union[List[int], str] - draworder: str - encoding: str - height: int - id: int - image: str - layers: List[Any] - name: str - objects: List[tiled_object.RawTiledObject] - offsetx: float - offsety: float - parallaxx: float - parallaxy: float - opacity: float - properties: List[properties_.RawProperty] - startx: int - starty: int - tintcolor: str - transparentcolor: str - type: str - visible: bool - width: int - x: int - y: int - - -def _convert_raw_tile_layer_data(data: List[int], layer_width: int) -> List[List[int]]: - """Convert raw layer data into a nested lit based on the layer width - - Args: - data: The data to convert - layer_width: Width of the layer - - Returns: - List[List[int]]: A nested list containing the converted data - """ - tile_grid: List[List[int]] = [[]] - - column_count = 0 - row_count = 0 - for item in data: - column_count += 1 - tile_grid[row_count].append(item) - if not column_count % layer_width and column_count < len(data): - row_count += 1 - tile_grid.append([]) - - return tile_grid - - -def _decode_tile_layer_data( - data: str, compression: str, layer_width: int -) -> List[List[int]]: - """Decode Base64 Encoded tile data. Optionally supports gzip and zlib compression. - - Args: - data: The base64 encoded data - compression: Either zlib, gzip, or empty. If empty no decompression is done. - - Returns: - List[List[int]]: A nested list containing the decoded data - - Raises: - ValueError: For an unsupported compression type. - """ - unencoded_data = base64.b64decode(data) - if compression == "zlib": - unzipped_data = zlib.decompress(unencoded_data) - elif compression == "gzip": - unzipped_data = gzip.decompress(unencoded_data) - elif compression == "zstd" and zstd is None: - raise ValueError( - "zstd compression support is not installed." - "To install use 'pip install pytiled-parser[zstd]'" - ) - elif compression == "zstd": - unzipped_data = zstd.decompress(unencoded_data) - else: - unzipped_data = unencoded_data - - tile_grid: List[int] = [] - - byte_count = 0 - int_count = 0 - int_value = 0 - for byte in unzipped_data: - int_value += byte << (byte_count * 8) - byte_count += 1 - if not byte_count % 4: - byte_count = 0 - int_count += 1 - tile_grid.append(int_value) - int_value = 0 - - return _convert_raw_tile_layer_data(tile_grid, layer_width) - - -def _cast_chunk( - raw_chunk: RawChunk, - encoding: Optional[str] = None, - compression: Optional[str] = None, -) -> Chunk: - """Cast the raw_chunk to a Chunk. - - Args: - raw_chunk: RawChunk to be casted to a Chunk - encoding: Encoding type. ("base64" or None) - compression: Either zlib, gzip, or empty. If empty no decompression is done. - - Returns: - Chunk: The Chunk created from the raw_chunk - """ - if encoding == "base64": - assert isinstance(compression, str) - assert isinstance(raw_chunk["data"], str) - data = _decode_tile_layer_data( - raw_chunk["data"], compression, raw_chunk["width"] - ) - else: - data = _convert_raw_tile_layer_data( - raw_chunk["data"], raw_chunk["width"] # type: ignore - ) - - chunk = Chunk( - coordinates=OrderedPair(raw_chunk["x"], raw_chunk["y"]), - size=Size(raw_chunk["width"], raw_chunk["height"]), - data=data, - ) - - return chunk - - -def _get_common_attributes(raw_layer: RawLayer) -> Layer: - """Create a Layer containing all the attributes common to all layers. - - This is to create the stub Layer object that can then be used to create the actual - specific sub-classes of Layer. - - Args: - raw_layer: Raw Tiled object get common attributes from - - Returns: - Layer: The attributes in common of all layers - """ - common_attributes = Layer( - name=raw_layer["name"], - opacity=raw_layer["opacity"], - visible=raw_layer["visible"], - ) - - # if startx is present, starty is present - if raw_layer.get("startx") is not None: - common_attributes.coordinates = OrderedPair( - raw_layer["startx"], raw_layer["starty"] - ) - - if raw_layer.get("id") is not None: - common_attributes.id = raw_layer["id"] - - # if either width or height is present, they both are - if raw_layer.get("width") is not None: - common_attributes.size = Size(raw_layer["width"], raw_layer["height"]) - - if raw_layer.get("offsetx") is not None: - common_attributes.offset = OrderedPair( - raw_layer["offsetx"], raw_layer["offsety"] - ) - - if raw_layer.get("properties") is not None: - common_attributes.properties = properties_.cast(raw_layer["properties"]) - - parallax = [1.0, 1.0] - - if raw_layer.get("parallaxx") is not None: - parallax[0] = raw_layer["parallaxx"] - - if raw_layer.get("parallaxy") is not None: - parallax[1] = raw_layer["parallaxy"] - - common_attributes.parallax_factor = OrderedPair(parallax[0], parallax[1]) - - if raw_layer.get("tintcolor") is not None: - common_attributes.tint_color = parse_color(raw_layer["tintcolor"]) - - return common_attributes - - -def _cast_tile_layer(raw_layer: RawLayer) -> TileLayer: - """Cast the raw_layer to a TileLayer. - - Args: - raw_layer: RawLayer to be casted to a TileLayer - - Returns: - TileLayer: The TileLayer created from raw_layer - """ - tile_layer = TileLayer(**_get_common_attributes(raw_layer).__dict__) - - if raw_layer.get("chunks") is not None: - tile_layer.chunks = [] - for chunk in raw_layer["chunks"]: - if raw_layer.get("encoding") is not None: - tile_layer.chunks.append( - _cast_chunk(chunk, raw_layer["encoding"], raw_layer["compression"]) - ) - else: - tile_layer.chunks.append(_cast_chunk(chunk)) - - if raw_layer.get("data") is not None: - if raw_layer.get("encoding") is not None: - tile_layer.data = _decode_tile_layer_data( - data=type_cast(str, raw_layer["data"]), - compression=raw_layer["compression"], - layer_width=raw_layer["width"], - ) - else: - tile_layer.data = _convert_raw_tile_layer_data( - raw_layer["data"], raw_layer["width"] # type: ignore - ) - - return tile_layer - - -def _cast_object_layer( - raw_layer: RawLayer, - parent_dir: Optional[Path] = None, -) -> ObjectLayer: - """Cast the raw_layer to an ObjectLayer. - - Args: - raw_layer: RawLayer to be casted to an ObjectLayer - Returns: - ObjectLayer: The ObjectLayer created from raw_layer - """ - - tiled_objects = [] - for tiled_object_ in raw_layer["objects"]: - tiled_objects.append(tiled_object.cast(tiled_object_, parent_dir)) - - return ObjectLayer( - tiled_objects=tiled_objects, - draw_order=raw_layer["draworder"], - **_get_common_attributes(raw_layer).__dict__, - ) - - -def _cast_image_layer(raw_layer: RawLayer) -> ImageLayer: - """Cast the raw_layer to a ImageLayer. - - Args: - raw_layer: RawLayer to be casted to a ImageLayer - - Returns: - ImageLayer: The ImageLayer created from raw_layer - """ - image_layer = ImageLayer( - image=Path(raw_layer["image"]), **_get_common_attributes(raw_layer).__dict__ - ) - - if raw_layer.get("transparentcolor") is not None: - image_layer.transparent_color = parse_color(raw_layer["transparentcolor"]) - - return image_layer - - -def _cast_group_layer( - raw_layer: RawLayer, parent_dir: Optional[Path] = None -) -> LayerGroup: - """Cast the raw_layer to a LayerGroup. - - Args: - raw_layer: RawLayer to be casted to a LayerGroup - - Returns: - LayerGroup: The LayerGroup created from raw_layer - """ - - layers = [] - - for layer in raw_layer["layers"]: - layers.append(cast(layer, parent_dir=parent_dir)) - - return LayerGroup(layers=layers, **_get_common_attributes(raw_layer).__dict__) - - -def cast( - raw_layer: RawLayer, - parent_dir: Optional[Path] = None, -) -> Layer: - """Cast a raw Tiled layer into a pytiled_parser type. - - This function will determine the type of layer and cast accordingly. - - Args: - raw_layer: Raw layer to be cast. - parent_dir: The parent directory that the map file is in. - - Returns: - Layer: a properly typed Layer. - - Raises: - RuntimeError: For an invalid layer type being provided - """ - type_ = raw_layer["type"] - - if type_ == "objectgroup": - return _cast_object_layer(raw_layer, parent_dir) - elif type_ == "group": - return _cast_group_layer(raw_layer, parent_dir) - elif type_ == "imagelayer": - return _cast_image_layer(raw_layer) - elif type_ == "tilelayer": - return _cast_tile_layer(raw_layer) - - raise RuntimeError(f"An invalid layer type of {type_} was supplied") diff --git a/pytiled_parser/parser.py b/pytiled_parser/parser.py new file mode 100644 index 00000000..7cdd4433 --- /dev/null +++ b/pytiled_parser/parser.py @@ -0,0 +1,17 @@ +from pathlib import Path + +from pytiled_parser.parsers.json.tiled_map import parse as json_map_parse +from pytiled_parser.tiled_map import TiledMap + + +def parse_map(file: Path) -> TiledMap: + """Parse the raw Tiled map into a pytiled_parser type + + Args: + file: Path to the map's JSON file + + Returns: + TileSet: a properly typed TileSet. + """ + # I have no idea why, but mypy thinks this function returns "Any" + return json_map_parse(file) # type: ignore diff --git a/pytiled_parser/parsers/json/layer.py b/pytiled_parser/parsers/json/layer.py new file mode 100644 index 00000000..49530454 --- /dev/null +++ b/pytiled_parser/parsers/json/layer.py @@ -0,0 +1,364 @@ +"""Layer parsing for the JSON Map Format. +""" +import base64 +import gzip +import importlib.util +import zlib +from pathlib import Path +from typing import Any, List, Optional, Union, cast + +from typing_extensions import TypedDict + +from pytiled_parser.common_types import OrderedPair, Size +from pytiled_parser.layer import ( + Chunk, + ImageLayer, + Layer, + LayerGroup, + ObjectLayer, + TileLayer, +) +from pytiled_parser.parsers.json.properties import RawProperty +from pytiled_parser.parsers.json.properties import parse as parse_properties +from pytiled_parser.parsers.json.tiled_object import RawObject +from pytiled_parser.parsers.json.tiled_object import parse as parse_object +from pytiled_parser.util import parse_color + +zstd_spec = importlib.util.find_spec("zstd") +if zstd_spec: + import zstd +else: + zstd = None + + +class RawChunk(TypedDict): + """The keys and their types that appear in a Tiled JSON Chunk Object. + + Tiled Doc: https://doc.mapeditor.org/en/stable/reference/json-map-format/#chunk + """ + + data: Union[List[int], str] + height: int + width: int + x: int + y: int + + +class RawLayer(TypedDict): + """The keys and their types that appear in a Tiled JSON Layer Object. + + Tiled Doc: https://doc.mapeditor.org/en/stable/reference/json-map-format/#layer + """ + + chunks: List[RawChunk] + compression: str + data: Union[List[int], str] + draworder: str + encoding: str + height: int + id: int + image: str + layers: List[Any] + name: str + objects: List[RawObject] + offsetx: float + offsety: float + parallaxx: float + parallaxy: float + opacity: float + properties: List[RawProperty] + startx: int + starty: int + tintcolor: str + transparentcolor: str + type: str + visible: bool + width: int + x: int + y: int + + +def _convert_raw_tile_layer_data(data: List[int], layer_width: int) -> List[List[int]]: + """Convert raw layer data into a nested lit based on the layer width + + Args: + data: The data to convert + layer_width: Width of the layer + + Returns: + List[List[int]]: A nested list containing the converted data + """ + tile_grid: List[List[int]] = [[]] + + column_count = 0 + row_count = 0 + for item in data: + column_count += 1 + tile_grid[row_count].append(item) + if not column_count % layer_width and column_count < len(data): + row_count += 1 + tile_grid.append([]) + + return tile_grid + + +def _decode_tile_layer_data( + data: str, compression: str, layer_width: int +) -> List[List[int]]: + """Decode Base64 Encoded tile data. Optionally supports gzip and zlib compression. + + Args: + data: The base64 encoded data + compression: Either zlib, gzip, or empty. If empty no decompression is done. + + Returns: + List[List[int]]: A nested list containing the decoded data + + Raises: + ValueError: For an unsupported compression type. + """ + unencoded_data = base64.b64decode(data) + if compression == "zlib": + unzipped_data = zlib.decompress(unencoded_data) + elif compression == "gzip": + unzipped_data = gzip.decompress(unencoded_data) + elif compression == "zstd" and zstd is None: + raise ValueError( + "zstd compression support is not installed." + "To install use 'pip install pytiled-parser[zstd]'" + ) + elif compression == "zstd": + unzipped_data = zstd.decompress(unencoded_data) + else: + unzipped_data = unencoded_data + + tile_grid: List[int] = [] + + byte_count = 0 + int_count = 0 + int_value = 0 + for byte in unzipped_data: + int_value += byte << (byte_count * 8) + byte_count += 1 + if not byte_count % 4: + byte_count = 0 + int_count += 1 + tile_grid.append(int_value) + int_value = 0 + + return _convert_raw_tile_layer_data(tile_grid, layer_width) + + +def _parse_chunk( + raw_chunk: RawChunk, + encoding: Optional[str] = None, + compression: Optional[str] = None, +) -> Chunk: + """Parse the raw_chunk to a Chunk. + + Args: + raw_chunk: RawChunk to be parsed to a Chunk + encoding: Encoding type. ("base64" or None) + compression: Either zlib, gzip, or empty. If empty no decompression is done. + + Returns: + Chunk: The Chunk created from the raw_chunk + """ + if encoding == "base64": + assert isinstance(compression, str) + assert isinstance(raw_chunk["data"], str) + data = _decode_tile_layer_data( + raw_chunk["data"], compression, raw_chunk["width"] + ) + else: + data = _convert_raw_tile_layer_data( + raw_chunk["data"], raw_chunk["width"] # type: ignore + ) + + chunk = Chunk( + coordinates=OrderedPair(raw_chunk["x"], raw_chunk["y"]), + size=Size(raw_chunk["width"], raw_chunk["height"]), + data=data, + ) + + return chunk + + +def _parse_common(raw_layer: RawLayer) -> Layer: + """Create a Layer containing all the attributes common to all layer types. + + This is to create the stub Layer object that can then be used to create the actual + specific sub-classes of Layer. + + Args: + raw_layer: Raw layer get common attributes from + + Returns: + Layer: The attributes in common of all layer types + """ + common = Layer( + name=raw_layer["name"], + opacity=raw_layer["opacity"], + visible=raw_layer["visible"], + ) + + # if startx is present, starty is present + if raw_layer.get("startx") is not None: + common.coordinates = OrderedPair(raw_layer["startx"], raw_layer["starty"]) + + if raw_layer.get("id") is not None: + common.id = raw_layer["id"] + + # if either width or height is present, they both are + if raw_layer.get("width") is not None: + common.size = Size(raw_layer["width"], raw_layer["height"]) + + if raw_layer.get("offsetx") is not None: + common.offset = OrderedPair(raw_layer["offsetx"], raw_layer["offsety"]) + + if raw_layer.get("properties") is not None: + common.properties = parse_properties(raw_layer["properties"]) + + parallax = [1.0, 1.0] + + if raw_layer.get("parallaxx") is not None: + parallax[0] = raw_layer["parallaxx"] + + if raw_layer.get("parallaxy") is not None: + parallax[1] = raw_layer["parallaxy"] + + common.parallax_factor = OrderedPair(parallax[0], parallax[1]) + + if raw_layer.get("tintcolor") is not None: + common.tint_color = parse_color(raw_layer["tintcolor"]) + + return common + + +def _parse_tile_layer(raw_layer: RawLayer) -> TileLayer: + """Parse the raw_layer to a TileLayer. + + Args: + raw_layer: RawLayer to be parsed to a TileLayer. + + Returns: + TileLayer: The TileLayer created from raw_layer + """ + tile_layer = TileLayer(**_parse_common(raw_layer).__dict__) + + if raw_layer.get("chunks") is not None: + tile_layer.chunks = [] + for chunk in raw_layer["chunks"]: + if raw_layer.get("encoding") is not None: + tile_layer.chunks.append( + _parse_chunk(chunk, raw_layer["encoding"], raw_layer["compression"]) + ) + else: + tile_layer.chunks.append(_parse_chunk(chunk)) + + if raw_layer.get("data") is not None: + if raw_layer.get("encoding") is not None: + tile_layer.data = _decode_tile_layer_data( + data=cast(str, raw_layer["data"]), + compression=raw_layer["compression"], + layer_width=raw_layer["width"], + ) + else: + tile_layer.data = _convert_raw_tile_layer_data( + raw_layer["data"], raw_layer["width"] # type: ignore + ) + + return tile_layer + + +def _parse_object_layer( + raw_layer: RawLayer, + parent_dir: Optional[Path] = None, +) -> ObjectLayer: + """Parse the raw_layer to an ObjectLayer. + + Args: + raw_layer: RawLayer to be parsed to an ObjectLayer. + + Returns: + ObjectLayer: The ObjectLayer created from raw_layer + """ + objects = [] + for object_ in raw_layer["objects"]: + objects.append(parse_object(object_, parent_dir)) + + return ObjectLayer( + tiled_objects=objects, + draw_order=raw_layer["draworder"], + **_parse_common(raw_layer).__dict__, + ) + + +def _parse_image_layer(raw_layer: RawLayer) -> ImageLayer: + """Parse the raw_layer to an ImageLayer. + + Args: + raw_layer: RawLayer to be parsed to an ImageLayer. + + Returns: + ImageLayer: The ImageLayer created from raw_layer + """ + image_layer = ImageLayer( + image=Path(raw_layer["image"]), **_parse_common(raw_layer).__dict__ + ) + + if raw_layer.get("transparentcolor") is not None: + image_layer.transparent_color = parse_color(raw_layer["transparentcolor"]) + + return image_layer + + +def _parse_group_layer( + raw_layer: RawLayer, parent_dir: Optional[Path] = None +) -> LayerGroup: + """Parse the raw_layer to a LayerGroup. + + Args: + raw_layer: RawLayer to be parsed to a LayerGroup. + + Returns: + LayerGroup: The LayerGroup created from raw_layer + """ + layers = [] + + for layer in raw_layer["layers"]: + layers.append(parse(layer, parent_dir=parent_dir)) + + return LayerGroup(layers=layers, **_parse_common(raw_layer).__dict__) + + +def parse( + raw_layer: RawLayer, + parent_dir: Optional[Path] = None, +) -> Layer: + """Parse a raw Layer into a pytiled_parser object. + + This function will determine the type of layer and parse accordingly. + + Args: + raw_layer: Raw layer to be parsed. + parent_dir: The parent directory that the map file is in. + + Returns: + Layer: A parsed Layer. + + Raises: + RuntimeError: For an invalid layer type being provided + """ + type_ = raw_layer["type"] + + if type_ == "objectgroup": + return _parse_object_layer(raw_layer, parent_dir) + elif type_ == "group": + return _parse_group_layer(raw_layer, parent_dir) + elif type_ == "imagelayer": + return _parse_image_layer(raw_layer) + elif type_ == "tilelayer": + return _parse_tile_layer(raw_layer) + + raise RuntimeError(f"An invalid layer type of {type_} was supplied") diff --git a/pytiled_parser/parsers/json/properties.py b/pytiled_parser/parsers/json/properties.py new file mode 100644 index 00000000..4e9896f3 --- /dev/null +++ b/pytiled_parser/parsers/json/properties.py @@ -0,0 +1,48 @@ +"""Property parsing for the JSON Map Format +""" + +from pathlib import Path +from typing import List, Union, cast + +from typing_extensions import TypedDict + +from pytiled_parser.properties import Properties, Property +from pytiled_parser.util import parse_color + +RawValue = Union[float, str, bool] + + +class RawProperty(TypedDict): + """The keys and their values that appear in a Tiled JSON Property Object. + + Tiled Docs: https://doc.mapeditor.org/en/stable/reference/json-map-format/#property + """ + + name: str + type: str + value: RawValue + + +def parse(raw_properties: List[RawProperty]) -> Properties: + """Parse a list of `RawProperty` objects into `Properties`. + + Args: + raw_properties: The list of `RawProperty` objects to parse. + + Returns: + Properties: The parsed `Property` objects. + """ + + final: Properties = {} + value: Property + + for raw_property in raw_properties: + if raw_property["type"] == "file": + value = Path(cast(str, raw_property["value"])) + elif raw_property["type"] == "color": + value = parse_color(cast(str, raw_property["value"])) + else: + value = raw_property["value"] + final[raw_property["name"]] = value + + return final diff --git a/pytiled_parser/parsers/json/tiled_map.py b/pytiled_parser/parsers/json/tiled_map.py new file mode 100644 index 00000000..304f577e --- /dev/null +++ b/pytiled_parser/parsers/json/tiled_map.py @@ -0,0 +1,153 @@ +import json +from pathlib import Path +from typing import List, Union, cast + +from typing_extensions import TypedDict + +from pytiled_parser.common_types import Size +from pytiled_parser.parsers.json.layer import RawLayer +from pytiled_parser.parsers.json.layer import parse as parse_layer +from pytiled_parser.parsers.json.properties import RawProperty +from pytiled_parser.parsers.json.properties import parse as parse_properties +from pytiled_parser.parsers.json.tileset import RawTileSet +from pytiled_parser.parsers.json.tileset import parse as parse_tileset +from pytiled_parser.tiled_map import TiledMap, TilesetDict +from pytiled_parser.util import parse_color + + +class RawTilesetMapping(TypedDict): + + firstgid: int + source: str + + +class RawTiledMap(TypedDict): + """The keys and their types that appear in a Tiled JSON Map Object. + + Tiled Docs: https://doc.mapeditor.org/en/stable/reference/json-map-format/#map + """ + + backgroundcolor: str + compressionlevel: int + height: int + hexsidelength: int + infinite: bool + layers: List[RawLayer] + nextlayerid: int + nextobjectid: int + orientation: str + properties: List[RawProperty] + renderorder: str + staggeraxis: str + staggerindex: str + tiledversion: str + tileheight: int + tilesets: List[RawTilesetMapping] + tilewidth: int + type: str + version: Union[str, float] + width: int + + +def parse(file: Path) -> TiledMap: + """Parse the raw Tiled map into a pytiled_parser type. + + Args: + file: Path to the map file. + + Returns: + TiledMap: A parsed TiledMap. + """ + with open(file) as map_file: + raw_tiled_map = json.load(map_file) + + parent_dir = file.parent + + raw_tilesets: List[Union[RawTileSet, RawTilesetMapping]] = raw_tiled_map["tilesets"] + tilesets: TilesetDict = {} + + for raw_tileset in raw_tilesets: + if raw_tileset.get("source") is not None: + # Is an external Tileset + tileset_path = Path(parent_dir / raw_tileset["source"]) + with open(tileset_path) as raw_tileset_file: + tilesets[raw_tileset["firstgid"]] = parse_tileset( + json.load(raw_tileset_file), + raw_tileset["firstgid"], + external_path=tileset_path.parent, + ) + else: + # Is an embedded Tileset + raw_tileset = cast(RawTileSet, raw_tileset) + tilesets[raw_tileset["firstgid"]] = parse_tileset( + raw_tileset, raw_tileset["firstgid"] + ) + + if isinstance(raw_tiled_map["version"], float): + version = str(raw_tiled_map["version"]) + else: + version = raw_tiled_map["version"] + + # `map` is a built-in function + map_ = TiledMap( + map_file=file, + infinite=raw_tiled_map["infinite"], + layers=[parse_layer(layer_, parent_dir) for layer_ in raw_tiled_map["layers"]], + map_size=Size(raw_tiled_map["width"], raw_tiled_map["height"]), + next_layer_id=raw_tiled_map["nextlayerid"], + next_object_id=raw_tiled_map["nextobjectid"], + orientation=raw_tiled_map["orientation"], + render_order=raw_tiled_map["renderorder"], + tiled_version=raw_tiled_map["tiledversion"], + tile_size=Size(raw_tiled_map["tilewidth"], raw_tiled_map["tileheight"]), + tilesets=tilesets, + version=version, + ) + + layers = [layer for layer in map_.layers if hasattr(layer, "tiled_objects")] + + for my_layer in layers: + for tiled_object in my_layer.tiled_objects: # type: ignore + if hasattr(tiled_object, "new_tileset"): + if tiled_object.new_tileset: + already_loaded = None + for val in map_.tilesets.values(): + if val.name == tiled_object.new_tileset["name"]: + already_loaded = val + break + + if not already_loaded: + highest_firstgid = max(map_.tilesets.keys()) + last_tileset_count = map_.tilesets[highest_firstgid].tile_count + new_firstgid = highest_firstgid + last_tileset_count + map_.tilesets[new_firstgid] = parse_tileset( + tiled_object.new_tileset, + new_firstgid, + tiled_object.new_tileset_path, + ) + tiled_object.gid = tiled_object.gid + (new_firstgid - 1) + + else: + tiled_object.gid = tiled_object.gid + ( + already_loaded.firstgid - 1 + ) + + tiled_object.new_tileset = None + tiled_object.new_tileset_path = None + + if raw_tiled_map.get("backgroundcolor") is not None: + map_.background_color = parse_color(raw_tiled_map["backgroundcolor"]) + + if raw_tiled_map.get("hexsidelength") is not None: + map_.hex_side_length = raw_tiled_map["hexsidelength"] + + if raw_tiled_map.get("properties") is not None: + map_.properties = parse_properties(raw_tiled_map["properties"]) + + if raw_tiled_map.get("staggeraxis") is not None: + map_.stagger_axis = raw_tiled_map["staggeraxis"] + + if raw_tiled_map.get("staggerindex") is not None: + map_.stagger_index = raw_tiled_map["staggerindex"] + + return map_ diff --git a/pytiled_parser/parsers/json/tiled_object.py b/pytiled_parser/parsers/json/tiled_object.py new file mode 100644 index 00000000..e5376373 --- /dev/null +++ b/pytiled_parser/parsers/json/tiled_object.py @@ -0,0 +1,321 @@ +"""Object parsing for the JSON Map Format. +""" +import json +from pathlib import Path +from typing import Any, Callable, Dict, List, Optional + +from typing_extensions import TypedDict + +from pytiled_parser.common_types import OrderedPair, Size +from pytiled_parser.parsers.json.properties import RawProperty +from pytiled_parser.parsers.json.properties import parse as parse_properties +from pytiled_parser.tiled_object import ( + Ellipse, + Point, + Polygon, + Polyline, + Rectangle, + Text, + Tile, + TiledObject, +) +from pytiled_parser.util import parse_color + + +class RawText(TypedDict): + """The keys and their types that appear in a Tiled JSON Text Object. + + Tiled Doc: https://doc.mapeditor.org/en/stable/reference/json-map-format/#text-example + """ + + text: str + color: str + + fontfamily: str + pixelsize: float # this is `font_size` in Text + + bold: bool + italic: bool + strikeout: bool + underline: bool + kerning: bool + + halign: str + valign: str + wrap: bool + + +class RawObject(TypedDict): + """The keys and their types that appear in a Tiled JSON Object. + + Tiled Doc: https://doc.mapeditor.org/en/stable/reference/json-map-format/#object + """ + + id: int + gid: int + template: str + x: float + y: float + width: float + height: float + rotation: float + visible: bool + name: str + type: str + properties: List[RawProperty] + ellipse: bool + point: bool + polygon: List[Dict[str, float]] + polyline: List[Dict[str, float]] + text: RawText + + +def _parse_common(raw_object: RawObject) -> TiledObject: + """Create an Object containing all the attributes common to all types of objects. + + Args: + raw_object: Raw object to get common attributes from + + Returns: + Object: The attributes in common of all types of objects + """ + + common = TiledObject( + id=raw_object["id"], + coordinates=OrderedPair(raw_object["x"], raw_object["y"]), + visible=raw_object["visible"], + size=Size(raw_object["width"], raw_object["height"]), + rotation=raw_object["rotation"], + name=raw_object["name"], + type=raw_object["type"], + ) + + if raw_object.get("properties") is not None: + common.properties = parse_properties(raw_object["properties"]) + + return common + + +def _parse_ellipse(raw_object: RawObject) -> Ellipse: + """Parse the raw object into an Ellipse. + + Args: + raw_object: Raw object to be parsed to an Ellipse + + Returns: + Ellipse: The Ellipse object created from the raw object + """ + return Ellipse(**_parse_common(raw_object).__dict__) + + +def _parse_rectangle(raw_object: RawObject) -> Rectangle: + """Parse the raw object into a Rectangle. + + Args: + raw_object: Raw object to be parsed to a Rectangle + + Returns: + Rectangle: The Rectangle object created from the raw object + """ + return Rectangle(**_parse_common(raw_object).__dict__) + + +def _parse_point(raw_object: RawObject) -> Point: + """Parse the raw object into a Point. + + Args: + raw_object: Raw object to be parsed to a Point + + Returns: + Point: The Point object created from the raw object + """ + return Point(**_parse_common(raw_object).__dict__) + + +def _parse_polygon(raw_object: RawObject) -> Polygon: + """Parse the raw object into a Polygon. + + Args: + raw_object: Raw object to be parsed to a Polygon + + Returns: + Polygon: The Polygon object created from the raw object + """ + polygon = [] + for point in raw_object["polygon"]: + polygon.append(OrderedPair(point["x"], point["y"])) + + return Polygon(points=polygon, **_parse_common(raw_object).__dict__) + + +def _parse_polyline(raw_object: RawObject) -> Polyline: + """Parse the raw object into a Polyline. + + Args: + raw_object: Raw object to be parsed to a Polyline + + Returns: + Polyline: The Polyline object created from the raw object + """ + polyline = [] + for point in raw_object["polyline"]: + polyline.append(OrderedPair(point["x"], point["y"])) + + return Polyline(points=polyline, **_parse_common(raw_object).__dict__) + + +def _parse_tile( + raw_object: RawObject, + new_tileset: Optional[Dict[str, Any]] = None, + new_tileset_path: Optional[Path] = None, +) -> Tile: + """Parse the raw object into a Tile. + + Args: + raw_object: Raw object to be parsed to a Tile + + Returns: + Tile: The Tile object created from the raw object + """ + gid = raw_object["gid"] + + return Tile( + gid=gid, + new_tileset=new_tileset, + new_tileset_path=new_tileset_path, + **_parse_common(raw_object).__dict__ + ) + + +def _parse_text(raw_object: RawObject) -> Text: + """Parse the raw object into Text. + + Args: + raw_object: Raw object to be parsed to a Text + + Returns: + Text: The Text object created from the raw object + """ + # required attributes + raw_text: RawText = raw_object["text"] + text = raw_text["text"] + + # create base Text object + text_object = Text(text=text, **_parse_common(raw_object).__dict__) + + # optional attributes + if raw_text.get("color") is not None: + text_object.color = parse_color(raw_text["color"]) + + if raw_text.get("fontfamily") is not None: + text_object.font_family = raw_text["fontfamily"] + + if raw_text.get("pixelsize") is not None: + text_object.font_size = raw_text["pixelsize"] + + if raw_text.get("bold") is not None: + text_object.bold = raw_text["bold"] + + if raw_text.get("italic") is not None: + text_object.italic = raw_text["italic"] + + if raw_text.get("kerning") is not None: + text_object.kerning = raw_text["kerning"] + + if raw_text.get("strikeout") is not None: + text_object.strike_out = raw_text["strikeout"] + + if raw_text.get("underline") is not None: + text_object.underline = raw_text["underline"] + + if raw_text.get("halign") is not None: + text_object.horizontal_align = raw_text["halign"] + + if raw_text.get("valign") is not None: + text_object.vertical_align = raw_text["valign"] + + if raw_text.get("wrap") is not None: + text_object.wrap = raw_text["wrap"] + + return text_object + + +def _get_parser(raw_object: RawObject) -> Callable[[RawObject], TiledObject]: + """Get the parser function for a given raw object. + + Only used internally by the JSON parser. + + Args: + raw_object: Raw object that is analyzed to determine the parser function. + + Returns: + Callable[[RawObject], Object]: The parser function. + """ + if raw_object.get("ellipse"): + return _parse_ellipse + + if raw_object.get("point"): + return _parse_point + + if raw_object.get("gid"): + # Only tile objects have the `gid` key + return _parse_tile + + if raw_object.get("polygon"): + return _parse_polygon + + if raw_object.get("polyline"): + return _parse_polyline + + if raw_object.get("text"): + return _parse_text + + # If it's none of the above, rectangle is the only one left. + # Rectangle is the only object which has no special properties to signify that. + return _parse_rectangle + + +def parse( + raw_object: RawObject, + parent_dir: Optional[Path] = None, +) -> TiledObject: + """Parse the raw object into a pytiled_parser version + + Args: + raw_object: Raw object that is to be cast. + parent_dir: The parent directory that the map file is in. + + Returns: + Object: A parsed Object. + + Raises: + RuntimeError: When a parameter that is conditionally required was not sent. + """ + new_tileset = None + new_tileset_path = None + + if raw_object.get("template"): + if not parent_dir: + raise RuntimeError( + "A parent directory must be specified when using object templates." + ) + template_path = Path(parent_dir / raw_object["template"]) + with open(template_path) as raw_template_file: + template = json.load(raw_template_file) + if "tileset" in template: + tileset_path = Path( + template_path.parent / template["tileset"]["source"] + ) + with open(tileset_path) as raw_tileset_file: + new_tileset = json.load(raw_tileset_file) + new_tileset_path = tileset_path.parent + + loaded_template = template["object"] + for key in loaded_template: + if key != "id": + raw_object[key] = loaded_template[key] # type: ignore + + if raw_object.get("gid"): + return _parse_tile(raw_object, new_tileset, new_tileset_path) + + return _get_parser(raw_object)(raw_object) diff --git a/pytiled_parser/parsers/json/tileset.py b/pytiled_parser/parsers/json/tileset.py new file mode 100644 index 00000000..3206bacf --- /dev/null +++ b/pytiled_parser/parsers/json/tileset.py @@ -0,0 +1,272 @@ +from pathlib import Path +from typing import List, Optional, Union + +from typing_extensions import TypedDict + +from pytiled_parser.common_types import OrderedPair +from pytiled_parser.parsers.json.layer import RawLayer +from pytiled_parser.parsers.json.layer import parse as parse_layer +from pytiled_parser.parsers.json.properties import RawProperty +from pytiled_parser.parsers.json.properties import parse as parse_properties +from pytiled_parser.parsers.json.wang_set import RawWangSet +from pytiled_parser.parsers.json.wang_set import parse as parse_wangset +from pytiled_parser.tileset import Frame, Grid, Tile, Tileset, Transformations +from pytiled_parser.util import parse_color + + +class RawFrame(TypedDict): + """The keys and their types that appear in a Frame JSON Object.""" + + duration: int + tileid: int + + +class RawTileOffset(TypedDict): + """The keys and their types that appear in a TileOffset JSON Object.""" + + x: int + y: int + + +class RawTransformations(TypedDict): + """The keys and their types that appear in a Transformations JSON Object.""" + + hflip: bool + vflip: bool + rotate: bool + preferuntransformed: bool + + +class RawTile(TypedDict): + """The keys and their types that appear in a Tile JSON Object.""" + + animation: List[RawFrame] + id: int + image: str + imageheight: int + imagewidth: int + opacity: float + properties: List[RawProperty] + objectgroup: RawLayer + type: str + + +class RawGrid(TypedDict): + """The keys and their types that appear in a Grid JSON Object.""" + + height: int + width: int + orientation: str + + +class RawTileSet(TypedDict): + """The keys and their types that appear in a TileSet JSON Object.""" + + backgroundcolor: str + columns: int + firstgid: int + grid: RawGrid + image: str + imageheight: int + imagewidth: int + margin: int + name: str + properties: List[RawProperty] + source: str + spacing: int + tilecount: int + tiledversion: str + tileheight: int + tileoffset: RawTileOffset + tiles: List[RawTile] + tilewidth: int + transparentcolor: str + transformations: RawTransformations + version: Union[str, float] + wangsets: List[RawWangSet] + + +def _parse_frame(raw_frame: RawFrame) -> Frame: + """Parse the raw_frame to a Frame. + + Args: + raw_frame: RawFrame to be parsed to a Frame + + Returns: + Frame: The Frame created from the raw_frame + """ + + return Frame(duration=raw_frame["duration"], tile_id=raw_frame["tileid"]) + + +def _parse_tile_offset(raw_tile_offset: RawTileOffset) -> OrderedPair: + """Parse the raw_tile_offset to an OrderedPair. + + Args: + raw_tile_offset: RawTileOffset to be parsed to an OrderedPair + + Returns: + OrderedPair: The OrderedPair created from the raw_tile_offset + """ + + return OrderedPair(raw_tile_offset["x"], raw_tile_offset["y"]) + + +def _parse_transformations(raw_transformations: RawTransformations) -> Transformations: + """Parse the raw_transformations to a Transformations object. + + Args: + raw_transformations: RawTransformations to be parsed to a Transformations + + Returns: + Transformations: The Transformations created from the raw_transformations + """ + + return Transformations( + hflip=raw_transformations["hflip"], + vflip=raw_transformations["vflip"], + rotate=raw_transformations["rotate"], + prefer_untransformed=raw_transformations["preferuntransformed"], + ) + + +def _parse_grid(raw_grid: RawGrid) -> Grid: + """Parse the raw_grid to a Grid object. + + Args: + raw_grid: RawGrid to be parsed to a Grid + + Returns: + Grid: The Grid created from the raw_grid + """ + + return Grid( + orientation=raw_grid["orientation"], + width=raw_grid["width"], + height=raw_grid["height"], + ) + + +def _parse_tile(raw_tile: RawTile, external_path: Optional[Path] = None) -> Tile: + """Parse the raw_tile to a Tile object. + + Args: + raw_tile: RawTile to be parsed to a Tile + + Returns: + Tile: The Tile created from the raw_tile + """ + + id_ = raw_tile["id"] + tile = Tile(id=id_) + + if raw_tile.get("animation") is not None: + tile.animation = [] + for frame in raw_tile["animation"]: + tile.animation.append(_parse_frame(frame)) + + if raw_tile.get("objectgroup") is not None: + tile.objects = parse_layer(raw_tile["objectgroup"]) + + if raw_tile.get("properties") is not None: + tile.properties = parse_properties(raw_tile["properties"]) + + if raw_tile.get("image") is not None: + if external_path: + tile.image = Path(external_path / raw_tile["image"]).absolute().resolve() + else: + tile.image = Path(raw_tile["image"]) + + if raw_tile.get("imagewidth") is not None: + tile.image_width = raw_tile["imagewidth"] + + if raw_tile.get("imageheight") is not None: + tile.image_height = raw_tile["imageheight"] + + if raw_tile.get("type") is not None: + tile.type = raw_tile["type"] + + return tile + + +def parse( + raw_tileset: RawTileSet, + firstgid: int, + external_path: Optional[Path] = None, +) -> Tileset: + """Parse the raw tileset into a pytiled_parser type + + Args: + raw_tileset: Raw Tileset to be parsed. + firstgid: GID corresponding the first tile in the set. + external_path: The path to the tileset if it is not an embedded one. + + Returns: + TileSet: a properly typed TileSet. + """ + + tileset = Tileset( + name=raw_tileset["name"], + tile_count=raw_tileset["tilecount"], + tile_width=raw_tileset["tilewidth"], + tile_height=raw_tileset["tileheight"], + columns=raw_tileset["columns"], + spacing=raw_tileset["spacing"], + margin=raw_tileset["margin"], + firstgid=firstgid, + ) + + if raw_tileset.get("version") is not None: + if isinstance(raw_tileset["version"], float): + tileset.version = str(raw_tileset["version"]) + else: + tileset.version = raw_tileset["version"] + + if raw_tileset.get("tiledversion") is not None: + tileset.tiled_version = raw_tileset["tiledversion"] + + if raw_tileset.get("image") is not None: + if external_path: + tileset.image = ( + Path(external_path / raw_tileset["image"]).absolute().resolve() + ) + else: + tileset.image = Path(raw_tileset["image"]) + + if raw_tileset.get("imagewidth") is not None: + tileset.image_width = raw_tileset["imagewidth"] + + if raw_tileset.get("imageheight") is not None: + tileset.image_height = raw_tileset["imageheight"] + + if raw_tileset.get("backgroundcolor") is not None: + tileset.background_color = parse_color(raw_tileset["backgroundcolor"]) + + if raw_tileset.get("tileoffset") is not None: + tileset.tile_offset = _parse_tile_offset(raw_tileset["tileoffset"]) + + if raw_tileset.get("transparentcolor") is not None: + tileset.transparent_color = parse_color(raw_tileset["transparentcolor"]) + + if raw_tileset.get("grid") is not None: + tileset.grid = _parse_grid(raw_tileset["grid"]) + + if raw_tileset.get("properties") is not None: + tileset.properties = parse_properties(raw_tileset["properties"]) + + if raw_tileset.get("tiles") is not None: + tiles = {} + for raw_tile in raw_tileset["tiles"]: + tiles[raw_tile["id"]] = _parse_tile(raw_tile, external_path=external_path) + tileset.tiles = tiles + + if raw_tileset.get("wangsets") is not None: + wangsets = [] + for raw_wangset in raw_tileset["wangsets"]: + wangsets.append(parse_wangset(raw_wangset)) + tileset.wang_sets = wangsets + + if raw_tileset.get("transformations") is not None: + tileset.transformations = _parse_transformations(raw_tileset["transformations"]) + + return tileset diff --git a/pytiled_parser/parsers/json/wang_set.py b/pytiled_parser/parsers/json/wang_set.py new file mode 100644 index 00000000..ea689051 --- /dev/null +++ b/pytiled_parser/parsers/json/wang_set.py @@ -0,0 +1,104 @@ +from typing import List + +from typing_extensions import TypedDict + +from pytiled_parser.parsers.json.properties import RawProperty +from pytiled_parser.parsers.json.properties import parse as parse_properties +from pytiled_parser.util import parse_color +from pytiled_parser.wang_set import WangColor, WangSet, WangTile + + +class RawWangTile(TypedDict): + """The keys and their types that appear in a Wang Tile JSON Object.""" + + tileid: int + # Tiled stores these IDs as a list represented like so: + # [top, top_right, right, bottom_right, bottom, bottom_left, left, top_left] + wangid: List[int] + + +class RawWangColor(TypedDict): + """The keys and their types that appear in a Wang Color JSON Object.""" + + color: str + name: str + probability: float + tile: int + properties: List[RawProperty] + + +class RawWangSet(TypedDict): + """The keys and their types that appear in a Wang Set JSON Object.""" + + colors: List[RawWangColor] + name: str + properties: List[RawProperty] + tile: int + type: str + wangtiles: List[RawWangTile] + + +def _parse_wang_tile(raw_wang_tile: RawWangTile) -> WangTile: + """Parse the raw wang tile into a pytiled_parser type + + Args: + raw_wang_tile: RawWangTile to be parsed. + + Returns: + WangTile: A properly typed WangTile. + """ + return WangTile(tile_id=raw_wang_tile["tileid"], wang_id=raw_wang_tile["wangid"]) + + +def _parse_wang_color(raw_wang_color: RawWangColor) -> WangColor: + """Parse the raw wang color into a pytiled_parser type + + Args: + raw_wang_color: RawWangColor to be parsed. + + Returns: + WangColor: A properly typed WangColor. + """ + wang_color = WangColor( + name=raw_wang_color["name"], + color=parse_color(raw_wang_color["color"]), + tile=raw_wang_color["tile"], + probability=raw_wang_color["probability"], + ) + + if raw_wang_color.get("properties") is not None: + wang_color.properties = parse_properties(raw_wang_color["properties"]) + + return wang_color + + +def parse(raw_wangset: RawWangSet) -> WangSet: + """Parse the raw wangset into a pytiled_parser type + + Args: + raw_wangset: Raw Wangset to be parsed. + + Returns: + WangSet: A properly typed WangSet. + """ + + colors = [] + for raw_wang_color in raw_wangset["colors"]: + colors.append(_parse_wang_color(raw_wang_color)) + + tiles = {} + for raw_wang_tile in raw_wangset["wangtiles"]: + tiles[raw_wang_tile["tileid"]] = _parse_wang_tile(raw_wang_tile) + + wangset = WangSet( + name=raw_wangset["name"], + tile=raw_wangset["tile"], + wang_type=raw_wangset["type"], + wang_colors=colors, + wang_tiles=tiles, + ) + + if raw_wangset.get("properties") is not None: + wangset.properties = parse_properties(raw_wangset["properties"]) + + return wangset diff --git a/pytiled_parser/properties.py b/pytiled_parser/properties.py index e9b87b5a..f8bc0acf 100644 --- a/pytiled_parser/properties.py +++ b/pytiled_parser/properties.py @@ -1,55 +1,18 @@ """Properties Module -This module casts raw properties from Tiled maps into a dictionary of -properly typed Properties. +This module defines types for Property objects. +For more about properties in Tiled maps see the below link: +https://doc.mapeditor.org/en/stable/manual/custom-properties/ + +The types defined in this module get added to other objects +such as Layers, Maps, Objects, etc """ from pathlib import Path -from typing import Dict, List, Union -from typing import cast as type_cast - -from typing_extensions import TypedDict +from typing import Dict, Union from .common_types import Color -from .util import parse_color Property = Union[float, Path, str, bool, Color] - Properties = Dict[str, Property] - - -RawValue = Union[float, str, bool] - - -class RawProperty(TypedDict): - """A dictionary of raw properties.""" - - name: str - type: str - value: RawValue - - -def cast(raw_properties: List[RawProperty]) -> Properties: - """Cast a list of `RawProperty`s into `Properties` - - Args: - raw_properties: The list of `RawProperty`s to cast. - - Returns: - Properties: The casted `Properties`. - """ - - final: Properties = {} - value: Property - - for property_ in raw_properties: - if property_["type"] == "file": - value = Path(type_cast(str, property_["value"])) - elif property_["type"] == "color": - value = parse_color(type_cast(str, property_["value"])) - else: - value = property_["value"] - final[property_["name"]] = value - - return final diff --git a/pytiled_parser/tiled_map.py b/pytiled_parser/tiled_map.py index 1f568851..9c1c1ba9 100644 --- a/pytiled_parser/tiled_map.py +++ b/pytiled_parser/tiled_map.py @@ -1,19 +1,12 @@ -# pylint: disable=too-few-public-methods - -import json from pathlib import Path -from typing import Dict, List, Optional, Union -from typing import cast as typing_cast +from typing import Dict, List, Optional import attr -from typing_extensions import TypedDict -from . import layer, properties, tileset -from .common_types import Color, Size -from .layer import Layer, RawLayer -from .properties import Properties, RawProperty -from .tileset import RawTileSet, Tileset -from .util import parse_color +from pytiled_parser.common_types import Color, Size +from pytiled_parser.layer import Layer +from pytiled_parser.properties import Properties +from pytiled_parser.tileset import Tileset TilesetDict = Dict[int, Tileset] @@ -68,146 +61,3 @@ class TiledMap: hex_side_length: Optional[int] = None stagger_axis: Optional[str] = None stagger_index: Optional[str] = None - - -class _RawTilesetMapping(TypedDict): - """ The way that tilesets are stored in the Tiled JSON formatted map.""" - - firstgid: int - source: str - - -class _RawTiledMap(TypedDict): - """The keys and their types that appear in a Tiled JSON Map. - - Keys: - compressionlevel: not documented - https://github.com/bjorn/tiled/issues/2815 - """ - - backgroundcolor: str - compressionlevel: int - height: int - hexsidelength: int - infinite: bool - layers: List[RawLayer] - nextlayerid: int - nextobjectid: int - orientation: str - properties: List[RawProperty] - renderorder: str - staggeraxis: str - staggerindex: str - tiledversion: str - tileheight: int - tilesets: List[_RawTilesetMapping] - tilewidth: int - type: str - version: Union[str, float] - width: int - - -def parse_map(file: Path) -> TiledMap: - """Parse the raw Tiled map into a pytiled_parser type - - Args: - file: Path to the map's JSON file - - Returns: - TileSet: a properly typed TileSet. - """ - - with open(file) as map_file: - raw_tiled_map = json.load(map_file) - - parent_dir = file.parent - - raw_tilesets: List[Union[RawTileSet, _RawTilesetMapping]] = raw_tiled_map[ - "tilesets" - ] - tilesets: TilesetDict = {} - - for raw_tileset in raw_tilesets: - if raw_tileset.get("source") is not None: - # Is an external Tileset - tileset_path = Path(parent_dir / raw_tileset["source"]) - with open(tileset_path) as raw_tileset_file: - tilesets[raw_tileset["firstgid"]] = tileset.cast( - json.load(raw_tileset_file), - raw_tileset["firstgid"], - external_path=tileset_path.parent, - ) - else: - # Is an embedded Tileset - raw_tileset = typing_cast(RawTileSet, raw_tileset) - tilesets[raw_tileset["firstgid"]] = tileset.cast( - raw_tileset, raw_tileset["firstgid"] - ) - - if isinstance(raw_tiled_map["version"], float): - version = str(raw_tiled_map["version"]) - else: - version = raw_tiled_map["version"] - - # `map` is a built-in function - map_ = TiledMap( - map_file=file, - infinite=raw_tiled_map["infinite"], - layers=[layer.cast(layer_, parent_dir) for layer_ in raw_tiled_map["layers"]], - map_size=Size(raw_tiled_map["width"], raw_tiled_map["height"]), - next_layer_id=raw_tiled_map["nextlayerid"], - next_object_id=raw_tiled_map["nextobjectid"], - orientation=raw_tiled_map["orientation"], - render_order=raw_tiled_map["renderorder"], - tiled_version=raw_tiled_map["tiledversion"], - tile_size=Size(raw_tiled_map["tilewidth"], raw_tiled_map["tileheight"]), - tilesets=tilesets, - version=version, - ) - - layers = [layer for layer in map_.layers if hasattr(layer, "tiled_objects")] - - for my_layer in layers: - for tiled_object in my_layer.tiled_objects: # type: ignore - if hasattr(tiled_object, "new_tileset"): - if tiled_object.new_tileset: - already_loaded = None - for val in map_.tilesets.values(): - if val.name == tiled_object.new_tileset["name"]: - already_loaded = val - break - - if not already_loaded: - highest_firstgid = max(map_.tilesets.keys()) - last_tileset_count = map_.tilesets[highest_firstgid].tile_count - new_firstgid = highest_firstgid + last_tileset_count - map_.tilesets[new_firstgid] = tileset.cast( - tiled_object.new_tileset, - new_firstgid, - tiled_object.new_tileset_path, - ) - tiled_object.gid = tiled_object.gid + (new_firstgid - 1) - - else: - tiled_object.gid = tiled_object.gid + ( - already_loaded.firstgid - 1 - ) - - tiled_object.new_tileset = None - tiled_object.new_tileset_path = None - - if raw_tiled_map.get("backgroundcolor") is not None: - map_.background_color = parse_color(raw_tiled_map["backgroundcolor"]) - - if raw_tiled_map.get("hexsidelength") is not None: - map_.hex_side_length = raw_tiled_map["hexsidelength"] - - if raw_tiled_map.get("properties") is not None: - map_.properties = properties.cast(raw_tiled_map["properties"]) - - if raw_tiled_map.get("staggeraxis") is not None: - map_.stagger_axis = raw_tiled_map["staggeraxis"] - - if raw_tiled_map.get("staggerindex") is not None: - map_.stagger_index = raw_tiled_map["staggerindex"] - - return map_ diff --git a/pytiled_parser/tiled_object.py b/pytiled_parser/tiled_object.py index 3d167d0d..17de14eb 100644 --- a/pytiled_parser/tiled_object.py +++ b/pytiled_parser/tiled_object.py @@ -1,14 +1,11 @@ # pylint: disable=too-few-public-methods -import json from pathlib import Path -from typing import Any, Callable, Dict, List, Optional, Union +from typing import Any, Dict, List, Optional import attr -from typing_extensions import TypedDict from . import properties as properties_ from .common_types import Color, OrderedPair, Size -from .util import parse_color @attr.s(auto_attribs=True, kw_only=True) @@ -150,300 +147,3 @@ class Tile(TiledObject): gid: int new_tileset: Optional[Dict[str, Any]] = None new_tileset_path: Optional[Path] = None - - -class RawTextDict(TypedDict): - """ The keys and their types that appear in a Text JSON Object.""" - - text: str - color: str - - fontfamily: str - pixelsize: float # this is `font_size` in Text - - bold: bool - italic: bool - strikeout: bool - underline: bool - kerning: bool - - halign: str - valign: str - wrap: bool - - -class RawTiledObject(TypedDict): - """ The keys and their types that appear in a Tiled JSON Object.""" - - id: int - gid: int - template: str - x: float - y: float - width: float - height: float - rotation: float - visible: bool - name: str - type: str - properties: List[properties_.RawProperty] - ellipse: bool - point: bool - polygon: List[Dict[str, float]] - polyline: List[Dict[str, float]] - text: Dict[str, Union[float, str]] - - -RawTiledObjects = List[RawTiledObject] - - -def _get_common_attributes(raw_tiled_object: RawTiledObject) -> TiledObject: - """Create a TiledObject containing all the attributes common to all tiled objects - - Args: - raw_tiled_object: Raw Tiled object get common attributes from - - Returns: - TiledObject: The attributes in common of all Tiled Objects - """ - - common_attributes = TiledObject( - id=raw_tiled_object["id"], - coordinates=OrderedPair(raw_tiled_object["x"], raw_tiled_object["y"]), - visible=raw_tiled_object["visible"], - size=Size(raw_tiled_object["width"], raw_tiled_object["height"]), - rotation=raw_tiled_object["rotation"], - name=raw_tiled_object["name"], - type=raw_tiled_object["type"], - ) - - if raw_tiled_object.get("properties") is not None: - common_attributes.properties = properties_.cast(raw_tiled_object["properties"]) - - return common_attributes - - -def _cast_ellipse(raw_tiled_object: RawTiledObject) -> Ellipse: - """Cast the raw_tiled_object to an Ellipse object. - - Args: - raw_tiled_object: Raw Tiled object to be casted to an Ellipse - - Returns: - Ellipse: The Ellipse object created from the raw_tiled_object - """ - return Ellipse(**_get_common_attributes(raw_tiled_object).__dict__) - - -def _cast_rectangle(raw_tiled_object: RawTiledObject) -> Rectangle: - """Cast the raw_tiled_object to a Rectangle object. - - Args: - raw_tiled_object: Raw Tiled object to be casted to a Rectangle - - Returns: - Rectangle: The Rectangle object created from the raw_tiled_object - """ - return Rectangle(**_get_common_attributes(raw_tiled_object).__dict__) - - -def _cast_point(raw_tiled_object: RawTiledObject) -> Point: - """Cast the raw_tiled_object to a Point object. - - Args: - raw_tiled_object: Raw Tiled object to be casted to a Point - - Returns: - Point: The Point object created from the raw_tiled_object - """ - return Point(**_get_common_attributes(raw_tiled_object).__dict__) - - -def _cast_tile( - raw_tiled_object: RawTiledObject, - new_tileset: Optional[Dict[str, Any]] = None, - new_tileset_path: Optional[Path] = None, -) -> Tile: - """Cast the raw_tiled_object to a Tile object. - - Args: - raw_tiled_object: Raw Tiled object to be casted to a Tile - - Returns: - Tile: The Tile object created from the raw_tiled_object - """ - gid = raw_tiled_object["gid"] - - return Tile( - gid=gid, - new_tileset=new_tileset, - new_tileset_path=new_tileset_path, - **_get_common_attributes(raw_tiled_object).__dict__ - ) - - -def _cast_polygon(raw_tiled_object: RawTiledObject) -> Polygon: - """Cast the raw_tiled_object to a Polygon object. - - Args: - raw_tiled_object: Raw Tiled object to be casted to a Polygon - - Returns: - Polygon: The Polygon object created from the raw_tiled_object - """ - polygon = [] - for point in raw_tiled_object["polygon"]: - polygon.append(OrderedPair(point["x"], point["y"])) - - return Polygon(points=polygon, **_get_common_attributes(raw_tiled_object).__dict__) - - -def _cast_polyline(raw_tiled_object: RawTiledObject) -> Polyline: - """Cast the raw_tiled_object to a Polyline object. - - Args: - raw_tiled_object: Raw Tiled Object to be casted to a Polyline - - Returns: - Polyline: The Polyline object created from the raw_tiled_object - """ - polyline = [] - for point in raw_tiled_object["polyline"]: - polyline.append(OrderedPair(point["x"], point["y"])) - - return Polyline( - points=polyline, **_get_common_attributes(raw_tiled_object).__dict__ - ) - - -def _cast_text(raw_tiled_object: RawTiledObject) -> Text: - """Cast the raw_tiled_object to a Text object. - - Args: - raw_tiled_object: Raw Tiled object to be casted to a Text object - - Returns: - Text: The Text object created from the raw_tiled_object - """ - # required attributes - raw_text_dict: RawTextDict = raw_tiled_object["text"] - text = raw_text_dict["text"] - - # create base Text object - text_object = Text(text=text, **_get_common_attributes(raw_tiled_object).__dict__) - - # optional attributes - if raw_text_dict.get("color") is not None: - text_object.color = parse_color(raw_text_dict["color"]) - - if raw_text_dict.get("fontfamily") is not None: - text_object.font_family = raw_text_dict["fontfamily"] - - if raw_text_dict.get("pixelsize") is not None: - text_object.font_size = raw_text_dict["pixelsize"] - - if raw_text_dict.get("bold") is not None: - text_object.bold = raw_text_dict["bold"] - - if raw_text_dict.get("italic") is not None: - text_object.italic = raw_text_dict["italic"] - - if raw_text_dict.get("kerning") is not None: - text_object.kerning = raw_text_dict["kerning"] - - if raw_text_dict.get("strikeout") is not None: - text_object.strike_out = raw_text_dict["strikeout"] - - if raw_text_dict.get("underline") is not None: - text_object.underline = raw_text_dict["underline"] - - if raw_text_dict.get("halign") is not None: - text_object.horizontal_align = raw_text_dict["halign"] - - if raw_text_dict.get("valign") is not None: - text_object.vertical_align = raw_text_dict["valign"] - - if raw_text_dict.get("wrap") is not None: - text_object.wrap = raw_text_dict["wrap"] - - return text_object - - -def _get_caster( - raw_tiled_object: RawTiledObject, -) -> Callable[[RawTiledObject], TiledObject]: - """Get the caster function for the raw tiled object. - - Args: - raw_tiled_object: Raw Tiled object that is analysed to determine which caster - to return. - - Returns: - Callable[[RawTiledObject], TiledObject]: The caster function. - """ - if raw_tiled_object.get("ellipse"): - return _cast_ellipse - - if raw_tiled_object.get("point"): - return _cast_point - - if raw_tiled_object.get("gid"): - # Only Tile objects have the `gid` key (I think) - return _cast_tile - - if raw_tiled_object.get("polygon"): - return _cast_polygon - - if raw_tiled_object.get("polyline"): - return _cast_polyline - - if raw_tiled_object.get("text"): - return _cast_text - - return _cast_rectangle - - -def cast( - raw_tiled_object: RawTiledObject, - parent_dir: Optional[Path] = None, -) -> TiledObject: - """Cast the raw tiled object into a pytiled_parser type - - Args: - raw_tiled_object: Raw Tiled object that is to be cast. - parent_dir: The parent directory that the map file is in. - - Returns: - TiledObject: a properly typed Tiled object. - - Raises: - RuntimeError: When a required parameter was not sent based on a condition. - """ - new_tileset = None - new_tileset_path = None - - if raw_tiled_object.get("template"): - if not parent_dir: - raise RuntimeError( - "A parent directory must be specified when using object templates" - ) - template_path = Path(parent_dir / raw_tiled_object["template"]) - with open(template_path) as raw_template_file: - template = json.load(raw_template_file) - if "tileset" in template: - tileset_path = Path( - template_path.parent / template["tileset"]["source"] - ) - with open(tileset_path) as raw_tileset_file: - new_tileset = json.load(raw_tileset_file) - new_tileset_path = tileset_path.parent - - loaded_template = template["object"] - for key in loaded_template: - if key != "id": - raw_tiled_object[key] = loaded_template[key] # type: ignore - - if raw_tiled_object.get("gid"): - return _cast_tile(raw_tiled_object, new_tileset, new_tileset_path) - - return _get_caster(raw_tiled_object)(raw_tiled_object) diff --git a/pytiled_parser/tileset.py b/pytiled_parser/tileset.py index 25bd190c..de480135 100644 --- a/pytiled_parser/tileset.py +++ b/pytiled_parser/tileset.py @@ -1,16 +1,13 @@ # pylint: disable=too-few-public-methods from pathlib import Path -from typing import Dict, List, NamedTuple, Optional, Union +from typing import Dict, List, NamedTuple, Optional import attr -from typing_extensions import TypedDict from . import layer from . import properties as properties_ from .common_types import Color, OrderedPair -from .util import parse_color -from .wang_set import RawWangSet, WangSet -from .wang_set import cast as cast_wangset +from .wang_set import WangSet class Grid(NamedTuple): @@ -153,261 +150,3 @@ class Tileset: properties: Optional[properties_.Properties] = None tiles: Optional[Dict[int, Tile]] = None wang_sets: Optional[List[WangSet]] = None - - -class RawFrame(TypedDict): - """ The keys and their types that appear in a Frame JSON Object.""" - - duration: int - tileid: int - - -class RawTileOffset(TypedDict): - """ The keys and their types that appear in a TileOffset JSON Object.""" - - x: int - y: int - - -class RawTransformations(TypedDict): - """ The keys and their types that appear in a Transformations JSON Object.""" - - hflip: bool - vflip: bool - rotate: bool - preferuntransformed: bool - - -class RawTile(TypedDict): - """ The keys and their types that appear in a Tile JSON Object.""" - - animation: List[RawFrame] - id: int - image: str - imageheight: int - imagewidth: int - opacity: float - properties: List[properties_.RawProperty] - objectgroup: layer.RawLayer - type: str - - -class RawGrid(TypedDict): - """ The keys and their types that appear in a Grid JSON Object.""" - - height: int - width: int - orientation: str - - -class RawTileSet(TypedDict): - """ The keys and their types that appear in a TileSet JSON Object.""" - - backgroundcolor: str - columns: int - firstgid: int - grid: RawGrid - image: str - imageheight: int - imagewidth: int - margin: int - name: str - properties: List[properties_.RawProperty] - source: str - spacing: int - tilecount: int - tiledversion: str - tileheight: int - tileoffset: RawTileOffset - tiles: List[RawTile] - tilewidth: int - transparentcolor: str - transformations: RawTransformations - version: Union[str, float] - wangsets: List[RawWangSet] - - -def _cast_frame(raw_frame: RawFrame) -> Frame: - """Cast the raw_frame to a Frame. - - Args: - raw_frame: RawFrame to be casted to a Frame - - Returns: - Frame: The Frame created from the raw_frame - """ - - return Frame(duration=raw_frame["duration"], tile_id=raw_frame["tileid"]) - - -def _cast_tile_offset(raw_tile_offset: RawTileOffset) -> OrderedPair: - """Cast the raw_tile_offset to an OrderedPair. - - Args: - raw_tile_offset: RawTileOffset to be casted to an OrderedPair - - Returns: - OrderedPair: The OrderedPair created from the raw_tile_offset - """ - - return OrderedPair(raw_tile_offset["x"], raw_tile_offset["y"]) - - -def _cast_tile(raw_tile: RawTile, external_path: Optional[Path] = None) -> Tile: - """Cast the raw_tile to a Tile object. - - Args: - raw_tile: RawTile to be casted to a Tile - - Returns: - Tile: The Tile created from the raw_tile - """ - - id_ = raw_tile["id"] - tile = Tile(id=id_) - - if raw_tile.get("animation") is not None: - tile.animation = [] - for frame in raw_tile["animation"]: - tile.animation.append(_cast_frame(frame)) - - if raw_tile.get("objectgroup") is not None: - tile.objects = layer.cast(raw_tile["objectgroup"]) - - if raw_tile.get("properties") is not None: - tile.properties = properties_.cast(raw_tile["properties"]) - - if raw_tile.get("image") is not None: - if external_path: - tile.image = Path(external_path / raw_tile["image"]).absolute().resolve() - else: - tile.image = Path(raw_tile["image"]) - - if raw_tile.get("imagewidth") is not None: - tile.image_width = raw_tile["imagewidth"] - - if raw_tile.get("imageheight") is not None: - tile.image_height = raw_tile["imageheight"] - - if raw_tile.get("type") is not None: - tile.type = raw_tile["type"] - - return tile - - -def _cast_transformations(raw_transformations: RawTransformations) -> Transformations: - """Cast the raw_transformations to a Transformations object. - - Args: - raw_transformations: RawTransformations to be casted to a Transformations - - Returns: - Transformations: The Transformations created from the raw_transformations - """ - - return Transformations( - hflip=raw_transformations["hflip"], - vflip=raw_transformations["vflip"], - rotate=raw_transformations["rotate"], - prefer_untransformed=raw_transformations["preferuntransformed"], - ) - - -def _cast_grid(raw_grid: RawGrid) -> Grid: - """Cast the raw_grid to a Grid object. - - Args: - raw_grid: RawGrid to be casted to a Grid - - Returns: - Grid: The Grid created from the raw_grid - """ - - return Grid( - orientation=raw_grid["orientation"], - width=raw_grid["width"], - height=raw_grid["height"], - ) - - -def cast( - raw_tileset: RawTileSet, - firstgid: int, - external_path: Optional[Path] = None, -) -> Tileset: - """Cast the raw tileset into a pytiled_parser type - - Args: - raw_tileset: Raw Tileset to be cast. - firstgid: GID corresponding the first tile in the set. - external_path: The path to the tileset if it is not an embedded one. - - Returns: - TileSet: a properly typed TileSet. - """ - - tileset = Tileset( - name=raw_tileset["name"], - tile_count=raw_tileset["tilecount"], - tile_width=raw_tileset["tilewidth"], - tile_height=raw_tileset["tileheight"], - columns=raw_tileset["columns"], - spacing=raw_tileset["spacing"], - margin=raw_tileset["margin"], - firstgid=firstgid, - ) - - if raw_tileset.get("version") is not None: - if isinstance(raw_tileset["version"], float): - tileset.version = str(raw_tileset["version"]) - else: - tileset.version = raw_tileset["version"] - - if raw_tileset.get("tiledversion") is not None: - tileset.tiled_version = raw_tileset["tiledversion"] - - if raw_tileset.get("image") is not None: - if external_path: - tileset.image = ( - Path(external_path / raw_tileset["image"]).absolute().resolve() - ) - else: - tileset.image = Path(raw_tileset["image"]) - - if raw_tileset.get("imagewidth") is not None: - tileset.image_width = raw_tileset["imagewidth"] - - if raw_tileset.get("imageheight") is not None: - tileset.image_height = raw_tileset["imageheight"] - - if raw_tileset.get("backgroundcolor") is not None: - tileset.background_color = parse_color(raw_tileset["backgroundcolor"]) - - if raw_tileset.get("tileoffset") is not None: - tileset.tile_offset = _cast_tile_offset(raw_tileset["tileoffset"]) - - if raw_tileset.get("transparentcolor") is not None: - tileset.transparent_color = parse_color(raw_tileset["transparentcolor"]) - - if raw_tileset.get("grid") is not None: - tileset.grid = _cast_grid(raw_tileset["grid"]) - - if raw_tileset.get("properties") is not None: - tileset.properties = properties_.cast(raw_tileset["properties"]) - - if raw_tileset.get("tiles") is not None: - tiles = {} - for raw_tile in raw_tileset["tiles"]: - tiles[raw_tile["id"]] = _cast_tile(raw_tile, external_path=external_path) - tileset.tiles = tiles - - if raw_tileset.get("wangsets") is not None: - wangsets = [] - for raw_wangset in raw_tileset["wangsets"]: - wangsets.append(cast_wangset(raw_wangset)) - tileset.wang_sets = wangsets - - if raw_tileset.get("transformations") is not None: - tileset.transformations = _cast_transformations(raw_tileset["transformations"]) - - return tileset diff --git a/pytiled_parser/version.py b/pytiled_parser/version.py index b7bdf02f..b06d5b07 100644 --- a/pytiled_parser/version.py +++ b/pytiled_parser/version.py @@ -1,3 +1,3 @@ """pytiled_parser version""" -__version__ = "1.5.4" +__version__ = "2.0.0-beta" diff --git a/pytiled_parser/wang_set.py b/pytiled_parser/wang_set.py index 011410fe..92417426 100644 --- a/pytiled_parser/wang_set.py +++ b/pytiled_parser/wang_set.py @@ -1,11 +1,9 @@ from typing import Dict, List, Optional import attr -from typing_extensions import TypedDict -from . import properties as properties_ -from .common_types import Color -from .util import parse_color +from pytiled_parser.common_types import Color +from pytiled_parser.properties import Properties @attr.s(auto_attribs=True) @@ -22,7 +20,7 @@ class WangColor: name: str probability: float tile: int - properties: Optional[properties_.Properties] = None + properties: Optional[Properties] = None @attr.s(auto_attribs=True) @@ -33,100 +31,4 @@ class WangSet: wang_type: str wang_tiles: Dict[int, WangTile] wang_colors: List[WangColor] - properties: Optional[properties_.Properties] = None - - -class RawWangTile(TypedDict): - """ The keys and their types that appear in a Wang Tile JSON Object.""" - - tileid: int - # Tiled stores these IDs as a list represented like so: - # [top, top_right, right, bottom_right, bottom, bottom_left, left, top_left] - wangid: List[int] - - -class RawWangColor(TypedDict): - """ The keys and their types that appear in a Wang Color JSON Object.""" - - color: str - name: str - probability: float - tile: int - properties: List[properties_.RawProperty] - - -class RawWangSet(TypedDict): - """ The keys and their types that appear in a Wang Set JSON Object.""" - - colors: List[RawWangColor] - name: str - properties: List[properties_.RawProperty] - tile: int - type: str - wangtiles: List[RawWangTile] - - -def _cast_wang_tile(raw_wang_tile: RawWangTile) -> WangTile: - """Cast the raw wang tile into a pytiled_parser type - - Args: - raw_wang_tile: RawWangTile to be cast. - - Returns: - WangTile: A properly typed WangTile. - """ - return WangTile(tile_id=raw_wang_tile["tileid"], wang_id=raw_wang_tile["wangid"]) - - -def _cast_wang_color(raw_wang_color: RawWangColor) -> WangColor: - """Cast the raw wang color into a pytiled_parser type - - Args: - raw_wang_color: RawWangColor to be cast. - - Returns: - WangColor: A properly typed WangColor. - """ - wang_color = WangColor( - name=raw_wang_color["name"], - color=parse_color(raw_wang_color["color"]), - tile=raw_wang_color["tile"], - probability=raw_wang_color["probability"], - ) - - if raw_wang_color.get("properties") is not None: - wang_color.properties = properties_.cast(raw_wang_color["properties"]) - - return wang_color - - -def cast(raw_wangset: RawWangSet) -> WangSet: - """Cast the raw wangset into a pytiled_parser type - - Args: - raw_wangset: Raw Wangset to be cast. - - Returns: - WangSet: A properly typed WangSet. - """ - - colors = [] - for raw_wang_color in raw_wangset["colors"]: - colors.append(_cast_wang_color(raw_wang_color)) - - tiles = {} - for raw_wang_tile in raw_wangset["wangtiles"]: - tiles[raw_wang_tile["tileid"]] = _cast_wang_tile(raw_wang_tile) - - wangset = WangSet( - name=raw_wangset["name"], - tile=raw_wangset["tile"], - wang_type=raw_wangset["type"], - wang_colors=colors, - wang_tiles=tiles, - ) - - if raw_wangset.get("properties") is not None: - wangset.properties = properties_.cast(raw_wangset["properties"]) - - return wangset + properties: Optional[Properties] = None diff --git a/pytiled_parser/world.py b/pytiled_parser/world.py index 5d35322f..797ff395 100644 --- a/pytiled_parser/world.py +++ b/pytiled_parser/world.py @@ -8,8 +8,9 @@ import attr from typing_extensions import TypedDict -from .common_types import OrderedPair, Size -from .tiled_map import TiledMap, parse_map +from pytiled_parser.common_types import OrderedPair, Size +from pytiled_parser.parser import parse_map +from pytiled_parser.tiled_map import TiledMap @attr.s(auto_attribs=True) @@ -55,7 +56,7 @@ class RawWorld(TypedDict): onlyShowAdjacentMaps: bool -def _cast_world_map(raw_world_map: RawWorldMap, map_file: Path) -> WorldMap: +def _parse_world_map(raw_world_map: RawWorldMap, map_file: Path) -> WorldMap: """Parse the RawWorldMap into a WorldMap. Args: @@ -94,7 +95,7 @@ def parse_world(file: Path) -> World: if raw_world.get("maps"): for raw_map in raw_world["maps"]: map_path = Path(parent_dir / raw_map["fileName"]) - maps.append(_cast_world_map(raw_map, map_path)) + maps.append(_parse_world_map(raw_map, map_path)) if raw_world.get("patterns"): for raw_pattern in raw_world["patterns"]: @@ -131,7 +132,7 @@ def parse_world(file: Path) -> World: } map_path = Path(parent_dir / map_file) - maps.append(_cast_world_map(raw_world_map, map_path)) + maps.append(_parse_world_map(raw_world_map, map_path)) world = World(maps=maps) diff --git a/setup.cfg b/setup.cfg index 86ce2325..2ccb3607 100644 --- a/setup.cfg +++ b/setup.cfg @@ -44,7 +44,7 @@ tests = pytest pytest-cov black - pylint + flake8 mypy isort<5,>=4.2.5 @@ -104,3 +104,7 @@ strict_optional = True [mypy-tests.*] ignore_errors = True + +[flake8] +max-line-length = 88 +exclude = .git,__pycache__,__init__.py,.mypy_cache,.pytest_cache diff --git a/tests/test_layer.py b/tests/test_layer.py index 117761b7..75cb733e 100644 --- a/tests/test_layer.py +++ b/tests/test_layer.py @@ -6,7 +6,7 @@ import pytest -from pytiled_parser import layer +from pytiled_parser.parsers.json.layer import parse TESTS_DIR = Path(os.path.dirname(os.path.abspath(__file__))) TEST_DATA = TESTS_DIR / "test_data" @@ -39,6 +39,6 @@ def test_layer_integration(layer_test): with open(raw_layers_path) as raw_layers_file: raw_layers = json.load(raw_layers_file)["layers"] - layers = [layer.cast(raw_layer) for raw_layer in raw_layers] + layers = [parse(raw_layer) for raw_layer in raw_layers] assert layers == expected.EXPECTED diff --git a/tests/test_map.py b/tests/test_map.py index 00192387..68ad9967 100644 --- a/tests/test_map.py +++ b/tests/test_map.py @@ -5,7 +5,7 @@ import pytest -from pytiled_parser import tiled_map +from pytiled_parser import parse_map TESTS_DIR = Path(os.path.dirname(os.path.abspath(__file__))) TEST_DATA = TESTS_DIR / "test_data" @@ -31,7 +31,7 @@ def test_map_integration(map_test): raw_maps_path = map_test / "map.json" - casted_map = tiled_map.parse_map(raw_maps_path) + casted_map = parse_map(raw_maps_path) expected.EXPECTED.map_file = casted_map.map_file assert casted_map == expected.EXPECTED diff --git a/tests/test_tiled_object.py b/tests/test_tiled_object.py index 10e71c4a..eacbc3ed 100644 --- a/tests/test_tiled_object.py +++ b/tests/test_tiled_object.py @@ -5,7 +5,17 @@ import pytest -from pytiled_parser import common_types, tiled_object +from pytiled_parser import common_types +from pytiled_parser.parsers.json.tiled_object import parse +from pytiled_parser.tiled_object import ( + Ellipse, + Point, + Polygon, + Polyline, + Rectangle, + Text, + Tile, +) ELLIPSES = [ ( @@ -23,7 +33,7 @@ "y":81.1913152210981 } """, - tiled_object.Ellipse( + Ellipse( id=6, size=common_types.Size(57.4013868364215, 18.5517790155735), name="name: ellipse", @@ -48,7 +58,7 @@ "y":53.9092872570194 } """, - tiled_object.Ellipse( + Ellipse( id=7, size=common_types.Size(6.32943048766625, 31.4288962146186), name="name: ellipse - invisible", @@ -73,7 +83,7 @@ "y":120.040923041946 } """, - tiled_object.Ellipse( + Ellipse( id=8, size=common_types.Size(29.6828464249176, 24.2264408321018), name="name: ellipse - rotated", @@ -98,7 +108,7 @@ "y":127.679890871888 } """, - tiled_object.Ellipse( + Ellipse( id=29, name="name: ellipse - no width or height", rotation=0, @@ -124,7 +134,7 @@ "y":23.571672160964 } """, - tiled_object.Rectangle( + Rectangle( id=1, size=common_types.Size(45.3972945322269, 41.4686825053996), name="name: rectangle", @@ -148,7 +158,7 @@ "y":91.0128452881664 } """, - tiled_object.Rectangle( + Rectangle( id=4, size=common_types.Size(30.9923837671934, 32.7384335568944), name="name: rectangle - invisible", @@ -172,7 +182,7 @@ "y":23.3534159372513 } """, - tiled_object.Rectangle( + Rectangle( id=5, size=common_types.Size(10, 22), name="name: rectangle - rotated", @@ -196,7 +206,7 @@ "y":53.4727748095942 } """, - tiled_object.Rectangle( + Rectangle( id=28, size=common_types.Size(0, 0), name="name: rectangle - no width or height", @@ -251,7 +261,7 @@ "y":131.826759122428 } """, - tiled_object.Rectangle( + Rectangle( id=30, size=common_types.Size(21.170853700125, 13.7501420938956), name="name: rectangle - properties", @@ -287,7 +297,7 @@ "y":82.9373650107991 } """, - tiled_object.Point( + Point( id=2, name="name: point", rotation=0, @@ -311,7 +321,7 @@ "y":95.8144822098443 } """, - tiled_object.Point( + Point( id=3, name="name: point invisible", rotation=0, @@ -338,7 +348,7 @@ "y":48.3019211094691 } """, - tiled_object.Tile( + Tile( id=13, size=common_types.Size(32, 32), name="name: tile", @@ -364,7 +374,7 @@ "y":168.779356598841 } """, - tiled_object.Tile( + Tile( id=14, size=common_types.Size(32, 32), name="name: tile - invisible", @@ -390,7 +400,7 @@ "y":59.8695009662385 } """, - tiled_object.Tile( + Tile( id=15, size=common_types.Size(32, 32), name="name: tile - horizontal flipped", @@ -416,7 +426,7 @@ "y":60.742525861089 } """, - tiled_object.Tile( + Tile( id=16, size=common_types.Size(32, 32), name="name: tile - vertical flipped", @@ -442,7 +452,7 @@ "y":95.6635216551097 } """, - tiled_object.Tile( + Tile( id=17, size=common_types.Size(32, 32), name="name: tile - both flipped", @@ -468,7 +478,7 @@ "y":142.62 } """, - tiled_object.Tile( + Tile( id=18, size=common_types.Size(32, 32), name="name: tile - rotated", @@ -517,7 +527,7 @@ "y":38.6313515971354 } """, - tiled_object.Polygon( + Polygon( id=9, name="name: polygon", points=[ @@ -560,7 +570,7 @@ "y":24.4446970558145 } """, - tiled_object.Polygon( + Polygon( id=10, name="name: polygon - invisible", points=[ @@ -613,7 +623,7 @@ "y":19.8613163578493 } """, - tiled_object.Polygon( + Polygon( id=11, name="name: polygon - rotated", points=[ @@ -660,7 +670,7 @@ "y":90.1398203933159 } """, - tiled_object.Polyline( + Polyline( id=12, name="name: polyline", points=[ @@ -701,7 +711,7 @@ "y":163.333333333333 } """, - tiled_object.Polyline( + Polyline( id=31, name="name: polyline - invisible", points=[ @@ -742,7 +752,7 @@ "y":128.666666666667 } """, - tiled_object.Polyline( + Polyline( id=32, name="name: polyline - rotated", points=[ @@ -778,7 +788,7 @@ "y":93.2986813686484 } """, - tiled_object.Text( + Text( id=19, name="name: text", text="Hello World", @@ -809,7 +819,7 @@ "y":112.068716607935 } """, - tiled_object.Text( + Text( id=20, name="name: text - invisible", text="Hello World", @@ -840,7 +850,7 @@ "y":78.4572581561896 } """, - tiled_object.Text( + Text( id=21, name="name: text - rotated", text="Hello World", @@ -874,7 +884,7 @@ "y":101.592417869728 } """, - tiled_object.Text( + Text( id=22, name="name: text - different font", text="Hello World", @@ -907,7 +917,7 @@ "y":154.192167784472 } """, - tiled_object.Text( + Text( id=23, name="name: text - no word wrap", text="Hello World", @@ -939,7 +949,7 @@ "y":1.19455496191883 } """, - tiled_object.Text( + Text( id=24, name="name: text - right bottom align", text="Hello World", @@ -973,7 +983,7 @@ "y": 3.81362964647039 } """, - tiled_object.Text( + Text( id=25, name="text: center center align", rotation=0, @@ -1006,7 +1016,7 @@ "y": 60.7785040354666 } """, - tiled_object.Text( + Text( id=26, name="name: text - justified", rotation=0, @@ -1038,7 +1048,7 @@ "y": 130.620495623508 } """, - tiled_object.Text( + Text( id=27, name="name: text - red", rotation=0, @@ -1075,7 +1085,7 @@ "y":22 } """, - tiled_object.Text( + Text( id=31, name="name: text - font options", rotation=0, @@ -1100,7 +1110,7 @@ @pytest.mark.parametrize("raw_object_json,expected", OBJECTS) def test_parse_layer(raw_object_json, expected): raw_object = json.loads(raw_object_json) - result = tiled_object.cast(raw_object) + result = parse(raw_object) assert result == expected @@ -1118,4 +1128,4 @@ def test_parse_no_parent_dir(): json_object = json.loads(raw_object) with pytest.raises(RuntimeError): - tiled_object.cast(json_object) + parse(json_object) diff --git a/tests/test_tileset.py b/tests/test_tileset.py index d24e9d59..c5b8f7ca 100644 --- a/tests/test_tileset.py +++ b/tests/test_tileset.py @@ -6,7 +6,7 @@ import pytest -from pytiled_parser import tileset +from pytiled_parser.parsers.json.tileset import parse TESTS_DIR = Path(os.path.dirname(os.path.abspath(__file__))) TEST_DATA = TESTS_DIR / "test_data" @@ -39,6 +39,6 @@ def test_tilesets_integration(tileset_dir): raw_tileset_path = tileset_dir / "tileset.json" with open(raw_tileset_path) as raw_tileset: - tileset_ = tileset.cast(json.loads(raw_tileset.read()), 1) + tileset_ = parse(json.loads(raw_tileset.read()), 1) assert tileset_ == expected.EXPECTED From d653ff63a3705ef7388361de8d55f5f7ded538ed Mon Sep 17 00:00:00 2001 From: Darren Eberly Date: Wed, 15 Dec 2021 20:59:25 -0500 Subject: [PATCH 02/13] Work on TMX parser. It is very not done --- pytiled_parser/__init__.py | 2 +- pytiled_parser/parser.py | 5 + pytiled_parser/parsers/tmx/layer.py | 117 ++++++++++++++ pytiled_parser/parsers/tmx/properties.py | 32 ++++ pytiled_parser/parsers/tmx/tiled_map.py | 59 +++++++ pytiled_parser/parsers/tmx/tileset.py | 186 +++++++++++++++++++++++ pytiled_parser/parsers/tmx/wang_set.py | 74 +++++++++ 7 files changed, 474 insertions(+), 1 deletion(-) create mode 100644 pytiled_parser/parsers/tmx/layer.py create mode 100644 pytiled_parser/parsers/tmx/properties.py create mode 100644 pytiled_parser/parsers/tmx/tiled_map.py create mode 100644 pytiled_parser/parsers/tmx/tileset.py create mode 100644 pytiled_parser/parsers/tmx/wang_set.py diff --git a/pytiled_parser/__init__.py b/pytiled_parser/__init__.py index a58b02f3..8d616bb4 100644 --- a/pytiled_parser/__init__.py +++ b/pytiled_parser/__init__.py @@ -13,7 +13,7 @@ from .common_types import OrderedPair, Size from .layer import ImageLayer, Layer, LayerGroup, ObjectLayer, TileLayer -from .parser import parse_map +from .parser import parse_map, parse_tmx from .properties import Properties from .tiled_map import TiledMap from .tileset import Tile, Tileset diff --git a/pytiled_parser/parser.py b/pytiled_parser/parser.py index 7cdd4433..69343f8f 100644 --- a/pytiled_parser/parser.py +++ b/pytiled_parser/parser.py @@ -1,6 +1,7 @@ from pathlib import Path from pytiled_parser.parsers.json.tiled_map import parse as json_map_parse +from pytiled_parser.parsers.tmx.tiled_map import parse as tmx_map_parse from pytiled_parser.tiled_map import TiledMap @@ -15,3 +16,7 @@ def parse_map(file: Path) -> TiledMap: """ # I have no idea why, but mypy thinks this function returns "Any" return json_map_parse(file) # type: ignore + + +def parse_tmx(file: Path) -> TiledMap: + return tmx_map_parse(file) # type: ignore diff --git a/pytiled_parser/parsers/tmx/layer.py b/pytiled_parser/parsers/tmx/layer.py new file mode 100644 index 00000000..21da21a6 --- /dev/null +++ b/pytiled_parser/parsers/tmx/layer.py @@ -0,0 +1,117 @@ +"""Layer parsing for the TMX Map Format. +""" +import xml.etree.ElementTree as etree +from pathlib import Path +from typing import Optional + +from pytiled_parser.common_types import OrderedPair, Size +from pytiled_parser.layer import ImageLayer, Layer +from pytiled_parser.parsers.tmx.properties import parse as parse_properties +from pytiled_parser.util import parse_color + + +def _parse_common(raw_layer: etree.Element) -> Layer: + """Create a Layer containing all the attributes common to all layer types. + + This is to create the stub Layer object that can then be used to create the actual + specific sub-classes of Layer. + + Args: + raw_layer: XML Element to get common attributes from + + Returns: + Layer: The attributes in common of all layer types + """ + common = Layer( + name=raw_layer.attrib["name"], + opacity=float(raw_layer.attrib["opacity"]), + visible=bool(int(raw_layer.attrib["visible"])), + ) + + if raw_layer.attrib.get("id") is not None: + common.id = int(raw_layer.attrib["id"]) + + if raw_layer.attrib.get("offsetx") is not None: + common.offset = OrderedPair( + float(raw_layer.attrib["offsetx"]), float(raw_layer.attrib["offsety"]) + ) + + properties_element = raw_layer.find("./properties") + if properties_element: + common.properties = parse_properties(properties_element) + + parallax = [1.0, 1.0] + + if raw_layer.attrib.get("parallaxx") is not None: + parallax[0] = float(raw_layer.attrib["parallaxx"]) + + if raw_layer.attrib.get("parallaxy") is not None: + parallax[1] = float(raw_layer.attrib["parallaxy"]) + + common.parallax_factor = OrderedPair(parallax[0], parallax[1]) + + if raw_layer.attrib.get("tintcolor") is not None: + common.tint_color = parse_color(raw_layer.attrib["tintcolor"]) + + return common + + +def _parse_image_layer(raw_layer: etree.Element) -> ImageLayer: + """Parse the raw_layer to an ImageLayer. + + Args: + raw_layer: XML Element to be parsed to an ImageLayer. + + Returns: + ImageLayer: The ImageLayer created from raw_layer + """ + image_element = raw_layer.find("./image") + if image_element: + source = Path(image_element.attrib["source"]) + width = int(image_element.attrib["width"]) + height = int(image_element.attrib["height"]) + + transparent_color = None + if image_element.attrib.get("trans") is not None: + transparent_color = parse_color(image_element.attrib["trans"]) + + return ImageLayer( + image=source, + size=Size(width, height), + transparent_color=transparent_color, + **_parse_common(raw_layer).__dict__, + ) + + raise RuntimeError("Tried to parse an image layer that doesn't have an image!") + + +def parse( + raw_layer: etree.Element, + parent_dir: Optional[Path] = None, +) -> Layer: + """Parse a raw Layer into a pytiled_parser object. + + This function will determine the type of layer and parse accordingly. + + Args: + raw_layer: Raw layer to be parsed. + parent_dir: The parent directory that the map file is in. + + Returns: + Layer: A parsed Layer. + + Raises: + RuntimeError: For an invalid layer type being provided + """ + type_ = raw_layer.tag + + if type_ == "objectgroup": + return _parse_object_layer(raw_layer, parent_dir) + elif type_ == "group": + return _parse_group_layer(raw_layer, parent_dir) + elif type_ == "imagelayer": + return _parse_image_layer(raw_layer) + elif type_ == "layer": + return _parse_tile_layer(raw_layer) + + raise RuntimeError(f"An invalid layer type of {type_} was supplied") diff --git a/pytiled_parser/parsers/tmx/properties.py b/pytiled_parser/parsers/tmx/properties.py new file mode 100644 index 00000000..b831a175 --- /dev/null +++ b/pytiled_parser/parsers/tmx/properties.py @@ -0,0 +1,32 @@ +import xml.etree.ElementTree as etree +from pathlib import Path +from typing import List, Union, cast + +from pytiled_parser.properties import Properties, Property +from pytiled_parser.util import parse_color + + +def parse(raw_properties: etree.Element) -> Properties: + + final: Properties = {} + value: Property + + for raw_property in raw_properties.findall("./property"): + type_ = raw_property.attrib["type"] + value_ = raw_property.attrib["value"] + if type_ == "file": + value = Path(value_) + elif type_ == "color": + value = parse_color(value_) + elif type_ == "int" or type_ == "float": + value = float(value_) + elif type_ == "bool": + if value_ == "true": + value = True + else: + value = False + else: + value = value_ + final[raw_property.attrib["name"]] = value + + return final diff --git a/pytiled_parser/parsers/tmx/tiled_map.py b/pytiled_parser/parsers/tmx/tiled_map.py new file mode 100644 index 00000000..a744495e --- /dev/null +++ b/pytiled_parser/parsers/tmx/tiled_map.py @@ -0,0 +1,59 @@ +import xml.etree.ElementTree as etree +from pathlib import Path + +from pytiled_parser.common_types import OrderedPair, Size +from pytiled_parser.parsers.tmx.tileset import parse as parse_tileset +from pytiled_parser.tiled_map import TiledMap, TilesetDict + + +def parse(file: Path) -> TiledMap: + """Parse the raw Tiled map into a pytiled_parser type. + + Args: + file: Path to the map file. + + Returns: + TiledMap: A parsed TiledMap. + """ + with open(file) as map_file: + raw_map = etree.parse(map_file).getroot() + + parent_dir = file.parent + + raw_tilesets = raw_map.findall("./tileset") + tilesets: TilesetDict = {} + + for raw_tileset in raw_tilesets: + if raw_tileset.attrib.get("source") is not None: + # Is an external Tileset + tileset_path = Path(parent_dir / raw_tileset.attrib["source"]) + with open(tileset_path) as tileset_file: + raw_tileset = etree.parse(tileset_file).getroot() + + tilesets[int(raw_tileset.attrib["firstgid"])] = parse_tileset( + raw_tileset, + int(raw_tileset.attrib["firstgid"]), + external_path=tileset_path.parent, + ) + else: + # Is an embedded Tileset + tilesets[int(raw_tileset.attrib["firstgid"])] = parse_tileset( + raw_tileset, int(raw_tileset.attrib["firstgid"]) + ) + + map_ = TiledMap( + map_file=file, + infinite=bool(int(raw_map.attrib["infinite"])), + layers=[parse_layer(layer_, parent_dir) for layer_ in raw_tiled_map["layers"]], + map_size=Size(int(raw_map.attrib["width"]), int(raw_map.attrib["height"])), + next_layer_id=int(raw_map.attrib["nextlayerid"]), + next_object_id=int(raw_map.attrib["nextobjectid"]), + orientation=raw_map.attrib["orientation"], + render_order=raw_map.attrib["renderorder"], + tiled_version=raw_map.attrib["tiledversion"], + tile_size=Size( + int(raw_map.attrib["tilewidth"]), int(raw_map.attrib["tileheight"]) + ), + tilesets=tilesets, + version=raw_map.attrib["version"], + ) diff --git a/pytiled_parser/parsers/tmx/tileset.py b/pytiled_parser/parsers/tmx/tileset.py new file mode 100644 index 00000000..3a123f70 --- /dev/null +++ b/pytiled_parser/parsers/tmx/tileset.py @@ -0,0 +1,186 @@ +import xml.etree.ElementTree as etree +from pathlib import Path +from typing import Optional + +from pytiled_parser.common_types import OrderedPair +from pytiled_parser.parsers.tmx.properties import parse as parse_properties +from pytiled_parser.parsers.tmx.wang_set import parse as parse_wangset +from pytiled_parser.tileset import Frame, Grid, Tile, Tileset, Transformations +from pytiled_parser.util import parse_color + + +def _parse_frame(raw_frame: etree.Element) -> Frame: + """Parse the raw_frame to a Frame object. + + Args: + raw_frame: XML Element to be parsed to a Frame + + Returns: + Frame: The Frame created from the raw_frame + """ + + return Frame( + duration=int(raw_frame.attrib["duration"]), + tile_id=int(raw_frame.attrib["tileid"]), + ) + + +def _parse_grid(raw_grid: etree.Element) -> Grid: + """Parse the raw_grid to a Grid object. + + Args: + raw_grid: XML Element to be parsed to a Grid + + Returns: + Grid: The Grid created from the raw_grid + """ + + return Grid( + orientation=raw_grid.attrib["orientation"], + width=int(raw_grid.attrib["width"]), + height=int(raw_grid.attrib["height"]), + ) + + +def _parse_transformations(raw_transformations: etree.Element) -> Transformations: + """Parse the raw_transformations to a Transformations object. + + Args: + raw_transformations: XML Element to be parsed to a Transformations + + Returns: + Transformations: The Transformations created from the raw_transformations + """ + + return Transformations( + hflip=bool(int(raw_transformations.attrib["hflip"])), + vflip=bool(int(raw_transformations.attrib["vflip"])), + rotate=bool(int(raw_transformations.attrib["rotate"])), + prefer_untransformed=bool( + int(raw_transformations.attrib["preferuntransformed"]) + ), + ) + + +def _parse_tile(raw_tile: etree.Element, external_path: Optional[Path] = None) -> Tile: + """Parse the raw_tile to a Tile object. + + Args: + raw_tile: XML Element to be parsed to a Tile + + Returns: + Tile: The Tile created from the raw_tile + """ + + tile = Tile(id=int(raw_tile.attrib["id"])) + + if raw_tile.attrib.get("type") is not None: + tile.type = raw_tile.attrib["type"] + + animation_element = raw_tile.find("./animation") + if animation_element: + tile.animation = [] + for raw_frame in animation_element.findall("./frame"): + tile.animation.append(_parse_frame(raw_frame)) + + properties_element = raw_tile.find("./properties") + if properties_element: + tile.properties = parse_properties(properties_element) + + image_element = raw_tile.find("./image") + if image_element: + if external_path: + tile.image = ( + Path(external_path / image_element.attrib["source"]) + .absolute() + .resolve() + ) + else: + tile.image = Path(image_element.attrib["source"]) + + tile.image_width = int(image_element.attrib["width"]) + tile.image_height = int(image_element.attrib["height"]) + + return tile + + +def parse( + raw_tileset: etree.Element, + firstgid: int, + external_path: Optional[Path] = None, +) -> Tileset: + tileset = Tileset( + name=raw_tileset.attrib["name"], + tile_count=int(raw_tileset.attrib["tilecount"]), + tile_width=int(raw_tileset.attrib["tilewidth"]), + tile_height=int(raw_tileset.attrib["tileheight"]), + columns=int(raw_tileset.attrib["columns"]), + spacing=int(raw_tileset.attrib["spacing"]), + margin=int(raw_tileset.attrib["margin"]), + firstgid=firstgid, + ) + + if raw_tileset.attrib.get("version") is not None: + tileset.version = raw_tileset.attrib["version"] + + if raw_tileset.attrib.get("tiledversion") is not None: + tileset.tiled_version = raw_tileset.attrib["tiledversion"] + + if raw_tileset.attrib.get("backgroundcolor") is not None: + tileset.background_color = parse_color(raw_tileset.attrib["backgroundcolor"]) + + image_element = raw_tileset.find("./image") + if image_element: + if external_path: + tileset.image = ( + Path(external_path / image_element.attrib["source"]) + .absolute() + .resolve() + ) + else: + tileset.image = Path(image_element.attrib["source"]) + + tileset.image_width = int(image_element.attrib["width"]) + tileset.image_height = int(image_element.attrib["height"]) + + if image_element.attrib.get("trans") is not None: + my_string = image_element.attrib["trans"] + if my_string[0] != "#": + my_string = f"#{my_string}" + tileset.transparent_color = parse_color(my_string) + pass + + tileoffset_element = raw_tileset.find("./tileoffset") + if tileoffset_element: + tileset.tile_offset = OrderedPair( + int(tileoffset_element.attrib["x"]), int(tileoffset_element.attrib["y"]) + ) + + grid_element = raw_tileset.find("./grid") + if grid_element: + tileset.grid = _parse_grid(grid_element) + + properties_element = raw_tileset.find("./properties") + if properties_element: + tileset.properties = parse_properties(properties_element) + + tiles = {} + for tile_element in raw_tileset.findall("./tiles"): + tiles[int(tile_element.attrib["id"])] = _parse_tile( + tile_element, external_path=external_path + ) + if tiles: + tileset.tiles = tiles + + wangsets_element = raw_tileset.find("./wangsets") + if wangsets_element: + wangsets = [] + for raw_wangset in wangsets_element.findall("./wangset"): + wangsets.append(parse_wangset(raw_wangset)) + tileset.wang_sets = wangsets + + transformations_element = raw_tileset.find("./transformations") + if transformations_element: + tileset.transformations = _parse_transformations(transformations_element) + + return tileset diff --git a/pytiled_parser/parsers/tmx/wang_set.py b/pytiled_parser/parsers/tmx/wang_set.py new file mode 100644 index 00000000..b1672262 --- /dev/null +++ b/pytiled_parser/parsers/tmx/wang_set.py @@ -0,0 +1,74 @@ +import xml.etree.ElementTree as etree + +from pytiled_parser.parsers.tmx.properties import parse as parse_properties +from pytiled_parser.util import parse_color +from pytiled_parser.wang_set import WangColor, WangSet, WangTile + + +def _parse_wang_tile(raw_wang_tile: etree.Element) -> WangTile: + """Parse the raw wang tile into a pytiled_parser type + + Args: + raw_wang_tile: XML Element to be parsed. + + Returns: + WangTile: A properly typed WangTile. + """ + ids = [int(v.strip()) for v in raw_wang_tile.attrib["wangid"].split(",")] + return WangTile(tile_id=int(raw_wang_tile.attrib["tileid"]), wang_id=ids) + + +def _parse_wang_color(raw_wang_color: etree.Element) -> WangColor: + """Parse the raw wang color into a pytiled_parser type + + Args: + raw_wang_color: XML Element to be parsed. + + Returns: + WangColor: A properly typed WangColor. + """ + wang_color = WangColor( + name=raw_wang_color.attrib["name"], + color=parse_color(raw_wang_color.attrib["color"]), + tile=int(raw_wang_color.attrib["tile"]), + probability=float(raw_wang_color.attrib["probability"]), + ) + + properties = raw_wang_color.find("./properties") + if properties: + wang_color.properties = parse_properties(properties) + + return wang_color + + +def parse(raw_wangset: etree.Element) -> WangSet: + """Parse the raw wangset into a pytiled_parser type + + Args: + raw_wangset: XML Element to be parsed. + + Returns: + WangSet: A properly typed WangSet. + """ + + colors = [] + for raw_wang_color in raw_wangset.findall("./wangcolor"): + colors.append(_parse_wang_color(raw_wang_color)) + + tiles = {} + for raw_wang_tile in raw_wangset.findall("./wangtile"): + tiles[int(raw_wang_tile.attrib["tileid"])] = _parse_wang_tile(raw_wang_tile) + + wangset = WangSet( + name=raw_wangset.attrib["name"], + tile=int(raw_wangset.attrib["tile"]), + wang_type=raw_wangset.attrib["type"], + wang_colors=colors, + wang_tiles=tiles, + ) + + properties = raw_wangset.find("./properties") + if properties: + wangset.properties = parse_properties(properties) + + return wangset From 115afb5e2297f64f3e45c59dbd0d968b68098ec4 Mon Sep 17 00:00:00 2001 From: Darren Eberly Date: Fri, 17 Dec 2021 01:09:40 -0500 Subject: [PATCH 03/13] More TMX work(it's mostly working I think) --- pytiled_parser/parsers/tmx/layer.py | 223 ++++++++++++++++- pytiled_parser/parsers/tmx/tiled_map.py | 59 ++++- pytiled_parser/parsers/tmx/tiled_object.py | 275 +++++++++++++++++++++ pytiled_parser/parsers/tmx/tileset.py | 5 + pytiled_parser/tiled_object.py | 5 +- 5 files changed, 562 insertions(+), 5 deletions(-) create mode 100644 pytiled_parser/parsers/tmx/tiled_object.py diff --git a/pytiled_parser/parsers/tmx/layer.py b/pytiled_parser/parsers/tmx/layer.py index 21da21a6..f825d0d7 100644 --- a/pytiled_parser/parsers/tmx/layer.py +++ b/pytiled_parser/parsers/tmx/layer.py @@ -1,14 +1,136 @@ """Layer parsing for the TMX Map Format. """ +import base64 +import gzip +import importlib.util import xml.etree.ElementTree as etree +import zlib from pathlib import Path -from typing import Optional +from typing import List, Optional from pytiled_parser.common_types import OrderedPair, Size -from pytiled_parser.layer import ImageLayer, Layer +from pytiled_parser.layer import ( + Chunk, + ImageLayer, + Layer, + LayerGroup, + ObjectLayer, + TileLayer, +) from pytiled_parser.parsers.tmx.properties import parse as parse_properties +from pytiled_parser.parsers.tmx.tiled_object import parse as parse_object from pytiled_parser.util import parse_color +zstd_spec = importlib.util.find_spec("zstd") +if zstd_spec: + import zstd +else: + zstd = None + + +def _convert_raw_tile_layer_data(data: List[int], layer_width: int) -> List[List[int]]: + """Convert raw layer data into a nested lit based on the layer width + + Args: + data: The data to convert + layer_width: Width of the layer + + Returns: + List[List[int]]: A nested list containing the converted data + """ + tile_grid: List[List[int]] = [[]] + + column_count = 0 + row_count = 0 + for item in data: + column_count += 1 + tile_grid[row_count].append(item) + if not column_count % layer_width and column_count < len(data): + row_count += 1 + tile_grid.append([]) + + return tile_grid + + +def _decode_tile_layer_data( + data: str, compression: str, layer_width: int +) -> List[List[int]]: + """Decode Base64 Encoded tile data. Optionally supports gzip and zlib compression. + + Args: + data: The base64 encoded data + compression: Either zlib, gzip, or empty. If empty no decompression is done. + + Returns: + List[List[int]]: A nested list containing the decoded data + + Raises: + ValueError: For an unsupported compression type. + """ + unencoded_data = base64.b64decode(data) + if compression == "zlib": + unzipped_data = zlib.decompress(unencoded_data) + elif compression == "gzip": + unzipped_data = gzip.decompress(unencoded_data) + elif compression == "zstd" and zstd is None: + raise ValueError( + "zstd compression support is not installed." + "To install use 'pip install pytiled-parser[zstd]'" + ) + elif compression == "zstd": + unzipped_data = zstd.decompress(unencoded_data) + else: + unzipped_data = unencoded_data + + tile_grid: List[int] = [] + + byte_count = 0 + int_count = 0 + int_value = 0 + for byte in unzipped_data: + int_value += byte << (byte_count * 8) + byte_count += 1 + if not byte_count % 4: + byte_count = 0 + int_count += 1 + tile_grid.append(int_value) + int_value = 0 + + return _convert_raw_tile_layer_data(tile_grid, layer_width) + + +def _parse_chunk( + raw_chunk: etree.Element, + encoding: Optional[str] = None, + compression: Optional[str] = None, +) -> Chunk: + """Parse the raw_chunk to a Chunk. + + Args: + raw_chunk: XML Element to be parsed to a Chunk + encoding: Encoding type. ("base64" or None) + compression: Either zlib, gzip, or empty. If empty no decompression is done. + + Returns: + Chunk: The Chunk created from the raw_chunk + """ + if encoding == "base64": + assert isinstance(compression, str) + data = _decode_tile_layer_data( + raw_chunk.text, compression, int(raw_chunk.attrib["width"]) # type: ignore + ) + else: + data = _convert_raw_tile_layer_data( + [int(v.strip) for v in raw_chunk.text], # type: ignore + int(raw_chunk.attrib["width"]), + ) + + return Chunk( + coordinates=OrderedPair(int(raw_chunk.attrib["x"]), int(raw_chunk.attrib["y"])), + size=Size(int(raw_chunk.attrib["width"]), int(raw_chunk.attrib["height"])), + data=data, + ) + def _parse_common(raw_layer: etree.Element) -> Layer: """Create a Layer containing all the attributes common to all layer types. @@ -56,6 +178,83 @@ def _parse_common(raw_layer: etree.Element) -> Layer: return common +def _parse_tile_layer(raw_layer: etree.Element) -> TileLayer: + """Parse the raw_layer to a TileLayer. + + Args: + raw_layer: XML Element to be parsed to a TileLayer. + + Returns: + TileLayer: The TileLayer created from raw_layer + """ + tile_layer = TileLayer( + size=Size(int(raw_layer.attrib["width"]), int(raw_layer.attrib["height"])), + **_parse_common(raw_layer).__dict__, + ) + + data_element = raw_layer.find("./data") + if data_element: + encoding = None + if data_element.attrib.get("encoding") is not None: + encoding = data_element.attrib["encoding"] + + compression = "" + if data_element.attrib.get("compression") is not None: + compression = data_element.attrib["compression"] + + raw_chunks = data_element.findall("./chunk") + + if not raw_chunks: + if encoding: + tile_layer.data = _decode_tile_layer_data( + data=data_element.text, # type: ignore + compression=compression, + layer_width=int(raw_layer.attrib["width"]), + ) + else: + tile_layer.data = _convert_raw_tile_layer_data( + [int(v.strip()) for v in data_element.text], # type: ignore + int(raw_layer.attrib["width"]), + ) + else: + chunks = [] + for raw_chunk in raw_chunks: + chunks.append( + _parse_chunk( + raw_chunk, + encoding, + compression, + ) + ) + + if chunks: + tile_layer.chunks = chunks + + return tile_layer + + +def _parse_object_layer( + raw_layer: etree.Element, parent_dir: Optional[Path] = None +) -> ObjectLayer: + """Parse the raw_layer to an ObjectLayer. + + Args: + raw_layer: XML Element to be parsed to an ObjectLayer. + + Returns: + ObjectLayer: The ObjectLayer created from raw_layer + """ + objects = [] + for object_ in raw_layer.findall("./object"): + objects.append(parse_object(object_, parent_dir)) + + return ObjectLayer( + tiled_objects=objects, + draw_order=raw_layer.attrib["draworder"], + **_parse_common(raw_layer).__dict__, + ) + + def _parse_image_layer(raw_layer: etree.Element) -> ImageLayer: """Parse the raw_layer to an ImageLayer. @@ -85,6 +284,26 @@ def _parse_image_layer(raw_layer: etree.Element) -> ImageLayer: raise RuntimeError("Tried to parse an image layer that doesn't have an image!") +def _parse_group_layer( + raw_layer: etree.Element, parent_dir: Optional[Path] = None +) -> LayerGroup: + """Parse the raw_layer to a LayerGroup. + + Args: + raw_layer: XML Element to be parsed to a LayerGroup. + + Returns: + LayerGroup: The LayerGroup created from raw_layer + """ + layers = [] + + for layer in raw_layer.iter(): + if layer.tag in ["layer", "objectgroup", "imagelayer", "group"]: + layers.append(parse(layer, parent_dir=parent_dir)) + + return LayerGroup(layers=layers, **_parse_common(raw_layer).__dict__) + + def parse( raw_layer: etree.Element, parent_dir: Optional[Path] = None, diff --git a/pytiled_parser/parsers/tmx/tiled_map.py b/pytiled_parser/parsers/tmx/tiled_map.py index a744495e..c352a25d 100644 --- a/pytiled_parser/parsers/tmx/tiled_map.py +++ b/pytiled_parser/parsers/tmx/tiled_map.py @@ -2,8 +2,11 @@ from pathlib import Path from pytiled_parser.common_types import OrderedPair, Size +from pytiled_parser.parsers.tmx.layer import parse as parse_layer +from pytiled_parser.parsers.tmx.properties import parse as parse_properties from pytiled_parser.parsers.tmx.tileset import parse as parse_tileset from pytiled_parser.tiled_map import TiledMap, TilesetDict +from pytiled_parser.util import parse_color def parse(file: Path) -> TiledMap: @@ -41,10 +44,15 @@ def parse(file: Path) -> TiledMap: raw_tileset, int(raw_tileset.attrib["firstgid"]) ) + layers = [] + for element in raw_map.iter(): + if element.tag in ["layer", "objectgroup", "imagelayer", "group"]: + layers.append(parse_layer(element, parent_dir)) + map_ = TiledMap( map_file=file, infinite=bool(int(raw_map.attrib["infinite"])), - layers=[parse_layer(layer_, parent_dir) for layer_ in raw_tiled_map["layers"]], + layers=layers, map_size=Size(int(raw_map.attrib["width"]), int(raw_map.attrib["height"])), next_layer_id=int(raw_map.attrib["nextlayerid"]), next_object_id=int(raw_map.attrib["nextobjectid"]), @@ -57,3 +65,52 @@ def parse(file: Path) -> TiledMap: tilesets=tilesets, version=raw_map.attrib["version"], ) + + layers = [layer for layer in map_.layers if hasattr(layer, "tiled_objects")] + + for my_layer in layers: + for tiled_object in my_layer.tiled_objects: + if hasattr(tiled_object, "new_tileset"): + if tiled_object.new_tileset: + already_loaded = None + for val in map_.tilesets.values(): + if val.name == tiled_object.new_tileset["name"]: + already_loaded = val + break + + if not already_loaded: + highest_firstgid = max(map_.tilesets.keys()) + last_tileset_count = map_.tilesets[highest_firstgid].tile_count + new_firstgid = highest_firstgid + last_tileset_count + map_.tilesets[new_firstgid] = parse_tileset( + tiled_object.new_tileset, + new_firstgid, + tiled_object.new_tileset_path, + ) + tiled_object.gid = tiled_object.gid + (new_firstgid - 1) + + else: + tiled_object.gid = tiled_object.gid + ( + already_loaded.firstgid - 1 + ) + + tiled_object.new_tileset = None + tiled_object.new_tileset_path = None + + if raw_map.attrib.get("backgroundcolor") is not None: + map_.background_color = parse_color(raw_map.attrib["backgroundcolor"]) + + if raw_map.attrib.get("hexsidelength") is not None: + map_.hex_side_length = int(raw_map.attrib["hexsidelength"]) + + properties_element = raw_map.find("./properties") + if properties_element: + map_.properties = parse_properties(properties_element) + + if raw_map.attrib.get("staggeraxis") is not None: + map_.stagger_axis = raw_map.attrib["staggeraxis"] + + if raw_map.attrib.get("staggerindex") is not None: + map_.stagger_index = raw_map.attrib["staggerindex"] + + return map_ diff --git a/pytiled_parser/parsers/tmx/tiled_object.py b/pytiled_parser/parsers/tmx/tiled_object.py new file mode 100644 index 00000000..1cf34301 --- /dev/null +++ b/pytiled_parser/parsers/tmx/tiled_object.py @@ -0,0 +1,275 @@ +import xml.etree.ElementTree as etree +from pathlib import Path +from typing import Callable, Optional + +from pytiled_parser.common_types import OrderedPair, Size +from pytiled_parser.parsers.tmx.properties import parse as parse_properties +from pytiled_parser.tiled_object import ( + Ellipse, + Point, + Polygon, + Polyline, + Rectangle, + Text, + Tile, + TiledObject, +) +from pytiled_parser.util import parse_color + + +def _parse_common(raw_object: etree.Element) -> TiledObject: + """Create an Object containing all the attributes common to all types of objects. + + Args: + raw_object: XML Element to get common attributes from + + Returns: + Object: The attributes in common of all types of objects + """ + + common = TiledObject( + id=int(raw_object.attrib["id"]), + coordinates=OrderedPair( + float(raw_object.attrib["x"]), float(raw_object.attrib["y"]) + ), + visible=bool(int(raw_object.attrib["visible"])), + size=Size( + float(raw_object.attrib["width"]), float(raw_object.attrib["height"]) + ), + rotation=float(raw_object.attrib["rotation"]), + name=raw_object.attrib["name"], + type=raw_object.attrib["type"], + ) + + properties_element = raw_object.find("./properties") + if properties_element: + common.properties = parse_properties(properties_element) + + return common + + +def _parse_ellipse(raw_object: etree.Element) -> Ellipse: + """Parse the raw object into an Ellipse. + + Args: + raw_object: XML Element to be parsed to an Ellipse + + Returns: + Ellipse: The Ellipse object created from the raw object + """ + return Ellipse(**_parse_common(raw_object).__dict__) + + +def _parse_rectangle(raw_object: etree.Element) -> Rectangle: + """Parse the raw object into a Rectangle. + + Args: + raw_object: XML Element to be parsed to a Rectangle + + Returns: + Rectangle: The Rectangle object created from the raw object + """ + return Rectangle(**_parse_common(raw_object).__dict__) + + +def _parse_point(raw_object: etree.Element) -> Point: + """Parse the raw object into a Point. + + Args: + raw_object: XML Element to be parsed to a Point + + Returns: + Point: The Point object created from the raw object + """ + return Point(**_parse_common(raw_object).__dict__) + + +def _parse_polygon(raw_object: etree.Element) -> Polygon: + """Parse the raw object into a Polygon. + + Args: + raw_object: XML Element to be parsed to a Polygon + + Returns: + Polygon: The Polygon object created from the raw object + """ + polygon = [] + for raw_point in raw_object.attrib["points"].split(" "): + point = raw_point.split(",") + polygon.append(OrderedPair(float(point[0]), float(point[1]))) + + return Polygon(points=polygon, **_parse_common(raw_object).__dict__) + + +def _parse_polyline(raw_object: etree.Element) -> Polyline: + """Parse the raw object into a Polyline. + + Args: + raw_object: Raw object to be parsed to a Polyline + + Returns: + Polyline: The Polyline object created from the raw object + """ + polyline = [] + for raw_point in raw_object.attrib["polyline"].split(" "): + point = raw_point.split(",") + polyline.append(OrderedPair(float(point[0]), float(point[1]))) + + return Polyline(points=polyline, **_parse_common(raw_object).__dict__) + + +def _parse_tile( + raw_object: etree.Element, + new_tileset: Optional[etree.Element] = None, + new_tileset_path: Optional[Path] = None, +) -> Tile: + """Parse the raw object into a Tile. + + Args: + raw_object: XML Element to be parsed to a Tile + + Returns: + Tile: The Tile object created from the raw object + """ + return Tile( + gid=int(raw_object.attrib["gid"]), + new_tileset=new_tileset, + new_tileset_path=new_tileset_path, + **_parse_common(raw_object).__dict__ + ) + + +def _parse_text(raw_object: etree.Element) -> Text: + """Parse the raw object into Text. + + Args: + raw_object: XML Element to be parsed to a Text + + Returns: + Text: The Text object created from the raw object + """ + # required attributes + text = raw_object.text + + if not text: + text = "" + # create base Text object + text_object = Text(text=text, **_parse_common(raw_object).__dict__) + + # optional attributes + if raw_object.attrib.get("color") is not None: + text_object.color = parse_color(raw_object.attrib["color"]) + + if raw_object.attrib.get("fontfamily") is not None: + text_object.font_family = raw_object.attrib["fontfamily"] + + if raw_object.attrib.get("pixelsize") is not None: + text_object.font_size = float(raw_object.attrib["pixelsize"]) + + if raw_object.attrib.get("bold") is not None: + text_object.bold = bool(int(raw_object.attrib["bold"])) + + if raw_object.attrib.get("italic") is not None: + text_object.italic = bool(int(raw_object.attrib["italic"])) + + if raw_object.attrib.get("kerning") is not None: + text_object.kerning = bool(int(raw_object.attrib["kerning"])) + + if raw_object.attrib.get("strikeout") is not None: + text_object.strike_out = bool(int(raw_object.attrib["strikeout"])) + + if raw_object.attrib.get("underline") is not None: + text_object.underline = bool(int(raw_object.attrib["underline"])) + + if raw_object.attrib.get("halign") is not None: + text_object.horizontal_align = raw_object.attrib["halign"] + + if raw_object.attrib.get("valign") is not None: + text_object.vertical_align = raw_object.attrib["valign"] + + if raw_object.attrib.get("wrap") is not None: + text_object.wrap = bool(int(raw_object.attrib["wrap"])) + + return text_object + + +def _get_parser(raw_object: etree.Element) -> Callable[[etree.Element], TiledObject]: + """Get the parser function for a given raw object. + + Only used internally by the TMX parser. + + Args: + raw_object: XML Element that is analyzed to determine the parser function. + + Returns: + Callable[[Element], Object]: The parser function. + """ + if raw_object.find("./ellipse"): + return _parse_ellipse + + if raw_object.find("./point"): + return _parse_point + + if raw_object.attrib.get("gid"): + # Only tile objects have the `gid` attribute + return _parse_tile + + if raw_object.find("./polygon"): + return _parse_polygon + + if raw_object.find("./polyline"): + return _parse_polyline + + if raw_object.find("./text"): + return _parse_text + + # If it's none of the above, rectangle is the only one left. + # Rectangle is the only object which has no properties to signify that. + return _parse_rectangle + + +def parse(raw_object: etree.Element, parent_dir: Optional[Path] = None) -> TiledObject: + """Parse the raw object into a pytiled_parser version + + Args: + raw_object: XML Element that is to be parsed. + parent_dir: The parent directory that the map file is in. + + Returns: + TiledObject: A parsed Object. + + Raises: + RuntimeError: When a parameter that is conditionally required was not sent. + """ + new_tileset = None + new_tileset_path = None + + if raw_object.attrib.get("template"): + if not parent_dir: + raise RuntimeError( + "A parent directory must be specified when using object templates." + ) + template_path = Path(parent_dir / raw_object.attrib["template"]) + with open(template_path) as template_file: + template = etree.parse(template_file).getroot() + + tileset_element = template.find("./tileset") + if tileset_element: + tileset_path = Path( + template_path.parent / tileset_element.attrib["source"] + ) + with open(tileset_path) as tileset_file: + new_tileset = etree.parse(tileset_file).getroot() + new_tileset_path = tileset_path.parent + + new_object = template.find("./object") + if raw_object.attrib.get("id") and new_object: + new_object.attrib["id"] = raw_object.attrib["id"] + + if new_object: + raw_object = new_object + + if raw_object.attrib.get("gid"): + return _parse_tile(raw_object, new_tileset, new_tileset_path) + + return _get_parser(raw_object)(raw_object) diff --git a/pytiled_parser/parsers/tmx/tileset.py b/pytiled_parser/parsers/tmx/tileset.py index 3a123f70..cdc2e850 100644 --- a/pytiled_parser/parsers/tmx/tileset.py +++ b/pytiled_parser/parsers/tmx/tileset.py @@ -3,6 +3,7 @@ from typing import Optional from pytiled_parser.common_types import OrderedPair +from pytiled_parser.parsers.tmx.layer import parse as parse_layer from pytiled_parser.parsers.tmx.properties import parse as parse_properties from pytiled_parser.parsers.tmx.wang_set import parse as parse_wangset from pytiled_parser.tileset import Frame, Grid, Tile, Tileset, Transformations @@ -83,6 +84,10 @@ def _parse_tile(raw_tile: etree.Element, external_path: Optional[Path] = None) - for raw_frame in animation_element.findall("./frame"): tile.animation.append(_parse_frame(raw_frame)) + object_element = raw_tile.find("./objectgroup") + if object_element: + tile.objects = parse_layer(object_element) + properties_element = raw_tile.find("./properties") if properties_element: tile.properties = parse_properties(properties_element) diff --git a/pytiled_parser/tiled_object.py b/pytiled_parser/tiled_object.py index 17de14eb..db5c6fc7 100644 --- a/pytiled_parser/tiled_object.py +++ b/pytiled_parser/tiled_object.py @@ -1,6 +1,7 @@ # pylint: disable=too-few-public-methods +import xml.etree.ElementTree as etree from pathlib import Path -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Optional, Union import attr @@ -145,5 +146,5 @@ class Tile(TiledObject): """ gid: int - new_tileset: Optional[Dict[str, Any]] = None + new_tileset: Optional[Union[etree.Element, Dict[str, Any]]] = None new_tileset_path: Optional[Path] = None From 65bfc2a498b37372bb159d6d7a20e559e989c09a Mon Sep 17 00:00:00 2001 From: Darren Eberly Date: Fri, 17 Dec 2021 20:04:11 -0500 Subject: [PATCH 04/13] First working TMX map parse at this point --- pytiled_parser/parsers/tmx/tiled_map.py | 4 ++-- pytiled_parser/parsers/tmx/tileset.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pytiled_parser/parsers/tmx/tiled_map.py b/pytiled_parser/parsers/tmx/tiled_map.py index c352a25d..8ecb1f8c 100644 --- a/pytiled_parser/parsers/tmx/tiled_map.py +++ b/pytiled_parser/parsers/tmx/tiled_map.py @@ -31,10 +31,10 @@ def parse(file: Path) -> TiledMap: # Is an external Tileset tileset_path = Path(parent_dir / raw_tileset.attrib["source"]) with open(tileset_path) as tileset_file: - raw_tileset = etree.parse(tileset_file).getroot() + raw_tileset_external = etree.parse(tileset_file).getroot() tilesets[int(raw_tileset.attrib["firstgid"])] = parse_tileset( - raw_tileset, + raw_tileset_external, int(raw_tileset.attrib["firstgid"]), external_path=tileset_path.parent, ) diff --git a/pytiled_parser/parsers/tmx/tileset.py b/pytiled_parser/parsers/tmx/tileset.py index cdc2e850..0d77b17a 100644 --- a/pytiled_parser/parsers/tmx/tileset.py +++ b/pytiled_parser/parsers/tmx/tileset.py @@ -134,8 +134,9 @@ def parse( if raw_tileset.attrib.get("backgroundcolor") is not None: tileset.background_color = parse_color(raw_tileset.attrib["backgroundcolor"]) - image_element = raw_tileset.find("./image") - if image_element: + image_element = raw_tileset.find("image") + if image_element is not None: + print("here") if external_path: tileset.image = ( Path(external_path / image_element.attrib["source"]) @@ -153,7 +154,6 @@ def parse( if my_string[0] != "#": my_string = f"#{my_string}" tileset.transparent_color = parse_color(my_string) - pass tileoffset_element = raw_tileset.find("./tileoffset") if tileoffset_element: From 2395dd32ca0166f1d2bcc5f77a68dd2ce8413860 Mon Sep 17 00:00:00 2001 From: Darren Eberly Date: Sun, 19 Dec 2021 01:43:42 -0500 Subject: [PATCH 05/13] More 2.0/tmx parsing work. TMX is nearly done --- pytiled_parser/layer.py | 4 +- pytiled_parser/parsers/tmx/layer.py | 38 ++++++++----- pytiled_parser/parsers/tmx/properties.py | 5 +- pytiled_parser/parsers/tmx/tiled_map.py | 5 +- pytiled_parser/parsers/tmx/tiled_object.py | 63 ++++++++++++++-------- pytiled_parser/parsers/tmx/tileset.py | 29 +++++----- pytiled_parser/tiled_object.py | 7 ++- 7 files changed, 93 insertions(+), 58 deletions(-) diff --git a/pytiled_parser/layer.py b/pytiled_parser/layer.py index 8a3bc9b5..a2cf80b4 100644 --- a/pytiled_parser/layer.py +++ b/pytiled_parser/layer.py @@ -38,8 +38,8 @@ class Layer: """ name: str - opacity: float - visible: bool + opacity: float = 1 + visible: bool = True coordinates: OrderedPair = OrderedPair(0, 0) parallax_factor: OrderedPair = OrderedPair(1, 1) diff --git a/pytiled_parser/parsers/tmx/layer.py b/pytiled_parser/parsers/tmx/layer.py index f825d0d7..3c4f0b1e 100644 --- a/pytiled_parser/parsers/tmx/layer.py +++ b/pytiled_parser/parsers/tmx/layer.py @@ -144,12 +144,19 @@ def _parse_common(raw_layer: etree.Element) -> Layer: Returns: Layer: The attributes in common of all layer types """ + if raw_layer.attrib.get("name") is None: + raw_layer.attrib["name"] = "" + common = Layer( name=raw_layer.attrib["name"], - opacity=float(raw_layer.attrib["opacity"]), - visible=bool(int(raw_layer.attrib["visible"])), ) + if raw_layer.attrib.get("opacity") is not None: + common.opacity = float(raw_layer.attrib["opacity"]) + + if raw_layer.attrib.get("visible") is not None: + common.visible = bool(int(raw_layer.attrib["visible"])) + if raw_layer.attrib.get("id") is not None: common.id = int(raw_layer.attrib["id"]) @@ -159,7 +166,7 @@ def _parse_common(raw_layer: etree.Element) -> Layer: ) properties_element = raw_layer.find("./properties") - if properties_element: + if properties_element is not None: common.properties = parse_properties(properties_element) parallax = [1.0, 1.0] @@ -187,13 +194,15 @@ def _parse_tile_layer(raw_layer: etree.Element) -> TileLayer: Returns: TileLayer: The TileLayer created from raw_layer """ + common = _parse_common(raw_layer).__dict__ + del common["size"] tile_layer = TileLayer( size=Size(int(raw_layer.attrib["width"]), int(raw_layer.attrib["height"])), - **_parse_common(raw_layer).__dict__, + **common, ) - data_element = raw_layer.find("./data") - if data_element: + data_element = raw_layer.find("data") + if data_element is not None: encoding = None if data_element.attrib.get("encoding") is not None: encoding = data_element.attrib["encoding"] @@ -202,10 +211,9 @@ def _parse_tile_layer(raw_layer: etree.Element) -> TileLayer: if data_element.attrib.get("compression") is not None: compression = data_element.attrib["compression"] - raw_chunks = data_element.findall("./chunk") - + raw_chunks = data_element.findall("chunk") if not raw_chunks: - if encoding: + if encoding and encoding != "csv": tile_layer.data = _decode_tile_layer_data( data=data_element.text, # type: ignore compression=compression, @@ -213,7 +221,7 @@ def _parse_tile_layer(raw_layer: etree.Element) -> TileLayer: ) else: tile_layer.data = _convert_raw_tile_layer_data( - [int(v.strip()) for v in data_element.text], # type: ignore + [int(v.strip()) for v in data_element.text.split(",")], # type: ignore int(raw_layer.attrib["width"]), ) else: @@ -248,12 +256,16 @@ def _parse_object_layer( for object_ in raw_layer.findall("./object"): objects.append(parse_object(object_, parent_dir)) - return ObjectLayer( + object_layer = ObjectLayer( tiled_objects=objects, - draw_order=raw_layer.attrib["draworder"], **_parse_common(raw_layer).__dict__, ) + if raw_layer.attrib.get("draworder") is not None: + object_layer.draw_order = raw_layer.attrib["draworder"] + + return object_layer + def _parse_image_layer(raw_layer: etree.Element) -> ImageLayer: """Parse the raw_layer to an ImageLayer. @@ -274,6 +286,8 @@ def _parse_image_layer(raw_layer: etree.Element) -> ImageLayer: if image_element.attrib.get("trans") is not None: transparent_color = parse_color(image_element.attrib["trans"]) + common = _parse_common(raw_layer).__dict__ + del common["size"] return ImageLayer( image=source, size=Size(width, height), diff --git a/pytiled_parser/parsers/tmx/properties.py b/pytiled_parser/parsers/tmx/properties.py index b831a175..173463b1 100644 --- a/pytiled_parser/parsers/tmx/properties.py +++ b/pytiled_parser/parsers/tmx/properties.py @@ -11,8 +11,9 @@ def parse(raw_properties: etree.Element) -> Properties: final: Properties = {} value: Property - for raw_property in raw_properties.findall("./property"): - type_ = raw_property.attrib["type"] + for raw_property in raw_properties.findall("property"): + + type_ = raw_property.attrib.get("type") value_ = raw_property.attrib["value"] if type_ == "file": value = Path(value_) diff --git a/pytiled_parser/parsers/tmx/tiled_map.py b/pytiled_parser/parsers/tmx/tiled_map.py index 8ecb1f8c..c44e916f 100644 --- a/pytiled_parser/parsers/tmx/tiled_map.py +++ b/pytiled_parser/parsers/tmx/tiled_map.py @@ -18,6 +18,7 @@ def parse(file: Path) -> TiledMap: Returns: TiledMap: A parsed TiledMap. """ + print(file) with open(file) as map_file: raw_map = etree.parse(map_file).getroot() @@ -71,10 +72,10 @@ def parse(file: Path) -> TiledMap: for my_layer in layers: for tiled_object in my_layer.tiled_objects: if hasattr(tiled_object, "new_tileset"): - if tiled_object.new_tileset: + if tiled_object.new_tileset is not None: already_loaded = None for val in map_.tilesets.values(): - if val.name == tiled_object.new_tileset["name"]: + if val.name == tiled_object.new_tileset.attrib["name"]: already_loaded = val break diff --git a/pytiled_parser/parsers/tmx/tiled_object.py b/pytiled_parser/parsers/tmx/tiled_object.py index 1cf34301..455c2c5e 100644 --- a/pytiled_parser/parsers/tmx/tiled_object.py +++ b/pytiled_parser/parsers/tmx/tiled_object.py @@ -32,15 +32,25 @@ def _parse_common(raw_object: etree.Element) -> TiledObject: coordinates=OrderedPair( float(raw_object.attrib["x"]), float(raw_object.attrib["y"]) ), - visible=bool(int(raw_object.attrib["visible"])), - size=Size( - float(raw_object.attrib["width"]), float(raw_object.attrib["height"]) - ), - rotation=float(raw_object.attrib["rotation"]), - name=raw_object.attrib["name"], - type=raw_object.attrib["type"], ) + if raw_object.attrib.get("width") is not None: + common.size = Size( + float(raw_object.attrib["width"]), float(raw_object.attrib["height"]) + ) + + if raw_object.attrib.get("visible") is not None: + common.visible = bool(int(raw_object.attrib["visible"])) + + if raw_object.attrib.get("rotation") is not None: + common.rotation = float(raw_object.attrib["rotation"]) + + if raw_object.attrib.get("name") is not None: + common.name = raw_object.attrib["name"] + + if raw_object.attrib.get("type") is not None: + common.type = raw_object.attrib["type"] + properties_element = raw_object.find("./properties") if properties_element: common.properties = parse_properties(properties_element) @@ -94,9 +104,11 @@ def _parse_polygon(raw_object: etree.Element) -> Polygon: Polygon: The Polygon object created from the raw object """ polygon = [] - for raw_point in raw_object.attrib["points"].split(" "): - point = raw_point.split(",") - polygon.append(OrderedPair(float(point[0]), float(point[1]))) + polygon_element = raw_object.find("./polygon") + if polygon_element is not None: + for raw_point in polygon_element.attrib["points"].split(" "): + point = raw_point.split(",") + polygon.append(OrderedPair(float(point[0]), float(point[1]))) return Polygon(points=polygon, **_parse_common(raw_object).__dict__) @@ -204,23 +216,19 @@ def _get_parser(raw_object: etree.Element) -> Callable[[etree.Element], TiledObj Returns: Callable[[Element], Object]: The parser function. """ - if raw_object.find("./ellipse"): + if raw_object.find("./ellipse") is not None: return _parse_ellipse - if raw_object.find("./point"): + if raw_object.find("./point") is not None: return _parse_point - if raw_object.attrib.get("gid"): - # Only tile objects have the `gid` attribute - return _parse_tile - - if raw_object.find("./polygon"): + if raw_object.find("./polygon") is not None: return _parse_polygon - if raw_object.find("./polyline"): + if raw_object.find("./polyline") is not None: return _parse_polyline - if raw_object.find("./text"): + if raw_object.find("./text") is not None: return _parse_text # If it's none of the above, rectangle is the only one left. @@ -263,13 +271,22 @@ def parse(raw_object: etree.Element, parent_dir: Optional[Path] = None) -> Tiled new_tileset_path = tileset_path.parent new_object = template.find("./object") - if raw_object.attrib.get("id") and new_object: - new_object.attrib["id"] = raw_object.attrib["id"] + if new_object is not None: + if raw_object.attrib.get("id") is not None: + new_object.attrib["id"] = raw_object.attrib["id"] + + if raw_object.attrib.get("x") is not None: + new_object.attrib["x"] = raw_object.attrib["x"] + + if raw_object.attrib.get("y") is not None: + new_object.attrib["y"] = raw_object.attrib["y"] - if new_object: raw_object = new_object + if raw_object.attrib.get("gid"): + return _parse_tile(raw_object, new_tileset, new_tileset_path) + if raw_object.attrib.get("gid"): - return _parse_tile(raw_object, new_tileset, new_tileset_path) + return _parse_tile(raw_object) return _get_parser(raw_object)(raw_object) diff --git a/pytiled_parser/parsers/tmx/tileset.py b/pytiled_parser/parsers/tmx/tileset.py index 0d77b17a..712d1cfa 100644 --- a/pytiled_parser/parsers/tmx/tileset.py +++ b/pytiled_parser/parsers/tmx/tileset.py @@ -79,21 +79,21 @@ def _parse_tile(raw_tile: etree.Element, external_path: Optional[Path] = None) - tile.type = raw_tile.attrib["type"] animation_element = raw_tile.find("./animation") - if animation_element: + if animation_element is not None: tile.animation = [] for raw_frame in animation_element.findall("./frame"): tile.animation.append(_parse_frame(raw_frame)) object_element = raw_tile.find("./objectgroup") - if object_element: + if object_element is not None: tile.objects = parse_layer(object_element) properties_element = raw_tile.find("./properties") - if properties_element: + if properties_element is not None: tile.properties = parse_properties(properties_element) image_element = raw_tile.find("./image") - if image_element: + if image_element is not None: if external_path: tile.image = ( Path(external_path / image_element.attrib["source"]) @@ -120,8 +120,6 @@ def parse( tile_width=int(raw_tileset.attrib["tilewidth"]), tile_height=int(raw_tileset.attrib["tileheight"]), columns=int(raw_tileset.attrib["columns"]), - spacing=int(raw_tileset.attrib["spacing"]), - margin=int(raw_tileset.attrib["margin"]), firstgid=firstgid, ) @@ -134,9 +132,14 @@ def parse( if raw_tileset.attrib.get("backgroundcolor") is not None: tileset.background_color = parse_color(raw_tileset.attrib["backgroundcolor"]) + if raw_tileset.attrib.get("spacing") is not None: + tileset.spacing = int(raw_tileset.attrib["spacing"]) + + if raw_tileset.attrib.get("margin") is not None: + tileset.margin = int(raw_tileset.attrib["margin"]) + image_element = raw_tileset.find("image") if image_element is not None: - print("here") if external_path: tileset.image = ( Path(external_path / image_element.attrib["source"]) @@ -156,21 +159,21 @@ def parse( tileset.transparent_color = parse_color(my_string) tileoffset_element = raw_tileset.find("./tileoffset") - if tileoffset_element: + if tileoffset_element is not None: tileset.tile_offset = OrderedPair( int(tileoffset_element.attrib["x"]), int(tileoffset_element.attrib["y"]) ) grid_element = raw_tileset.find("./grid") - if grid_element: + if grid_element is not None: tileset.grid = _parse_grid(grid_element) properties_element = raw_tileset.find("./properties") - if properties_element: + if properties_element is not None: tileset.properties = parse_properties(properties_element) tiles = {} - for tile_element in raw_tileset.findall("./tiles"): + for tile_element in raw_tileset.findall("./tile"): tiles[int(tile_element.attrib["id"])] = _parse_tile( tile_element, external_path=external_path ) @@ -178,14 +181,14 @@ def parse( tileset.tiles = tiles wangsets_element = raw_tileset.find("./wangsets") - if wangsets_element: + if wangsets_element is not None: wangsets = [] for raw_wangset in wangsets_element.findall("./wangset"): wangsets.append(parse_wangset(raw_wangset)) tileset.wang_sets = wangsets transformations_element = raw_tileset.find("./transformations") - if transformations_element: + if transformations_element is not None: tileset.transformations = _parse_transformations(transformations_element) return tileset diff --git a/pytiled_parser/tiled_object.py b/pytiled_parser/tiled_object.py index db5c6fc7..a96d65a7 100644 --- a/pytiled_parser/tiled_object.py +++ b/pytiled_parser/tiled_object.py @@ -35,10 +35,9 @@ class TiledObject: coordinates: OrderedPair size: Size = Size(0, 0) rotation: float = 0 - visible: bool - - name: Optional[str] = None - type: Optional[str] = None + visible: bool = True + name: str = "" + type: str = "" properties: properties_.Properties = {} From 5ce8bbafcb6c4d837125c13bdd60428f1e400f12 Mon Sep 17 00:00:00 2001 From: Darren Eberly Date: Sun, 19 Dec 2021 16:13:20 -0500 Subject: [PATCH 06/13] Automatic detection of map/tileset format --- pytiled_parser/__init__.py | 3 +- pytiled_parser/exception.py | 2 ++ pytiled_parser/parser.py | 21 +++++++++----- pytiled_parser/parsers/json/tiled_map.py | 35 +++++++++++++++++------ pytiled_parser/parsers/tmx/tiled_map.py | 36 ++++++++++++++++-------- pytiled_parser/util.py | 10 +++++++ 6 files changed, 79 insertions(+), 28 deletions(-) create mode 100644 pytiled_parser/exception.py diff --git a/pytiled_parser/__init__.py b/pytiled_parser/__init__.py index 8d616bb4..f37e2ae3 100644 --- a/pytiled_parser/__init__.py +++ b/pytiled_parser/__init__.py @@ -12,8 +12,9 @@ # pylint: disable=too-few-public-methods from .common_types import OrderedPair, Size +from .exception import UnknownFormat from .layer import ImageLayer, Layer, LayerGroup, ObjectLayer, TileLayer -from .parser import parse_map, parse_tmx +from .parser import parse_map from .properties import Properties from .tiled_map import TiledMap from .tileset import Tile, Tileset diff --git a/pytiled_parser/exception.py b/pytiled_parser/exception.py new file mode 100644 index 00000000..8d753547 --- /dev/null +++ b/pytiled_parser/exception.py @@ -0,0 +1,2 @@ +class UnknownFormat(Exception): + pass diff --git a/pytiled_parser/parser.py b/pytiled_parser/parser.py index 69343f8f..9a0a3892 100644 --- a/pytiled_parser/parser.py +++ b/pytiled_parser/parser.py @@ -1,22 +1,29 @@ from pathlib import Path +from pytiled_parser import UnknownFormat from pytiled_parser.parsers.json.tiled_map import parse as json_map_parse from pytiled_parser.parsers.tmx.tiled_map import parse as tmx_map_parse from pytiled_parser.tiled_map import TiledMap +from pytiled_parser.util import check_format def parse_map(file: Path) -> TiledMap: """Parse the raw Tiled map into a pytiled_parser type Args: - file: Path to the map's JSON file + file: Path to the map file Returns: - TileSet: a properly typed TileSet. + Tiledmap: a properly typed TiledMap """ - # I have no idea why, but mypy thinks this function returns "Any" - return json_map_parse(file) # type: ignore + parser = check_format(file) - -def parse_tmx(file: Path) -> TiledMap: - return tmx_map_parse(file) # type: ignore + # The type ignores are because mypy for some reaosn thinks those functions return Any + if parser == "tmx": + return tmx_map_parse(file) # type: ignore + elif parser == "json": + return json_map_parse(file) # type: ignore + else: + raise UnknownFormat( + "Unknown Map Format, please use either the TMX or JSON format." + ) diff --git a/pytiled_parser/parsers/json/tiled_map.py b/pytiled_parser/parsers/json/tiled_map.py index 304f577e..82f6c049 100644 --- a/pytiled_parser/parsers/json/tiled_map.py +++ b/pytiled_parser/parsers/json/tiled_map.py @@ -1,18 +1,21 @@ import json +import xml.etree.ElementTree as etree from pathlib import Path from typing import List, Union, cast from typing_extensions import TypedDict from pytiled_parser.common_types import Size +from pytiled_parser.exception import UnknownFormat from pytiled_parser.parsers.json.layer import RawLayer from pytiled_parser.parsers.json.layer import parse as parse_layer from pytiled_parser.parsers.json.properties import RawProperty from pytiled_parser.parsers.json.properties import parse as parse_properties from pytiled_parser.parsers.json.tileset import RawTileSet -from pytiled_parser.parsers.json.tileset import parse as parse_tileset +from pytiled_parser.parsers.json.tileset import parse as parse_json_tileset +from pytiled_parser.parsers.tmx.tileset import parse as parse_tmx_tileset from pytiled_parser.tiled_map import TiledMap, TilesetDict -from pytiled_parser.util import parse_color +from pytiled_parser.util import check_format, parse_color class RawTilesetMapping(TypedDict): @@ -70,16 +73,30 @@ def parse(file: Path) -> TiledMap: if raw_tileset.get("source") is not None: # Is an external Tileset tileset_path = Path(parent_dir / raw_tileset["source"]) + parser = check_format(tileset_path) with open(tileset_path) as raw_tileset_file: - tilesets[raw_tileset["firstgid"]] = parse_tileset( - json.load(raw_tileset_file), - raw_tileset["firstgid"], - external_path=tileset_path.parent, - ) + if parser == "json": + tilesets[raw_tileset["firstgid"]] = parse_json_tileset( + json.load(raw_tileset_file), + raw_tileset["firstgid"], + external_path=tileset_path.parent, + ) + elif parser == "tmx": + raw_tileset_external = etree.parse(raw_tileset_file).getroot() + tilesets[raw_tileset["firstgid"]] = parse_tmx_tileset( + raw_tileset_external, + raw_tileset["firstgid"], + external_path=tileset_path.parent, + ) + else: + raise UnknownFormat( + "Unkown Tileset format, please use either the TSX or JSON format." + ) + else: # Is an embedded Tileset raw_tileset = cast(RawTileSet, raw_tileset) - tilesets[raw_tileset["firstgid"]] = parse_tileset( + tilesets[raw_tileset["firstgid"]] = parse_json_tileset( raw_tileset, raw_tileset["firstgid"] ) @@ -120,7 +137,7 @@ def parse(file: Path) -> TiledMap: highest_firstgid = max(map_.tilesets.keys()) last_tileset_count = map_.tilesets[highest_firstgid].tile_count new_firstgid = highest_firstgid + last_tileset_count - map_.tilesets[new_firstgid] = parse_tileset( + map_.tilesets[new_firstgid] = parse_json_tileset( tiled_object.new_tileset, new_firstgid, tiled_object.new_tileset_path, diff --git a/pytiled_parser/parsers/tmx/tiled_map.py b/pytiled_parser/parsers/tmx/tiled_map.py index c44e916f..4ecb5ed3 100644 --- a/pytiled_parser/parsers/tmx/tiled_map.py +++ b/pytiled_parser/parsers/tmx/tiled_map.py @@ -1,12 +1,15 @@ +import json import xml.etree.ElementTree as etree from pathlib import Path from pytiled_parser.common_types import OrderedPair, Size +from pytiled_parser.exception import UnknownFormat +from pytiled_parser.parsers.json.tileset import parse as parse_json_tileset from pytiled_parser.parsers.tmx.layer import parse as parse_layer from pytiled_parser.parsers.tmx.properties import parse as parse_properties -from pytiled_parser.parsers.tmx.tileset import parse as parse_tileset +from pytiled_parser.parsers.tmx.tileset import parse as parse_tmx_tileset from pytiled_parser.tiled_map import TiledMap, TilesetDict -from pytiled_parser.util import parse_color +from pytiled_parser.util import check_format, parse_color def parse(file: Path) -> TiledMap: @@ -18,7 +21,6 @@ def parse(file: Path) -> TiledMap: Returns: TiledMap: A parsed TiledMap. """ - print(file) with open(file) as map_file: raw_map = etree.parse(map_file).getroot() @@ -31,17 +33,29 @@ def parse(file: Path) -> TiledMap: if raw_tileset.attrib.get("source") is not None: # Is an external Tileset tileset_path = Path(parent_dir / raw_tileset.attrib["source"]) + parser = check_format(tileset_path) with open(tileset_path) as tileset_file: - raw_tileset_external = etree.parse(tileset_file).getroot() + if parser == "tmx": + raw_tileset_external = etree.parse(tileset_file).getroot() + tilesets[int(raw_tileset.attrib["firstgid"])] = parse_tmx_tileset( + raw_tileset_external, + int(raw_tileset.attrib["firstgid"]), + external_path=tileset_path.parent, + ) + elif parser == "json": + tilesets[int(raw_tileset.attrib["firstgid"])] = parse_json_tileset( + json.load(tileset_file), + int(raw_tileset.attrib["firstgid"]), + external_path=tileset_path.parent, + ) + else: + raise UnknownFormat( + "Unkown Tileset format, please use either the TSX or JSON format." + ) - tilesets[int(raw_tileset.attrib["firstgid"])] = parse_tileset( - raw_tileset_external, - int(raw_tileset.attrib["firstgid"]), - external_path=tileset_path.parent, - ) else: # Is an embedded Tileset - tilesets[int(raw_tileset.attrib["firstgid"])] = parse_tileset( + tilesets[int(raw_tileset.attrib["firstgid"])] = parse_tmx_tileset( raw_tileset, int(raw_tileset.attrib["firstgid"]) ) @@ -83,7 +97,7 @@ def parse(file: Path) -> TiledMap: highest_firstgid = max(map_.tilesets.keys()) last_tileset_count = map_.tilesets[highest_firstgid].tile_count new_firstgid = highest_firstgid + last_tileset_count - map_.tilesets[new_firstgid] = parse_tileset( + map_.tilesets[new_firstgid] = parse_tmx_tileset( tiled_object.new_tileset, new_firstgid, tiled_object.new_tileset_path, diff --git a/pytiled_parser/util.py b/pytiled_parser/util.py index 75c7d1db..07fd9554 100644 --- a/pytiled_parser/util.py +++ b/pytiled_parser/util.py @@ -1,4 +1,5 @@ """Utility Functions for PyTiled""" +from pathlib import Path from pytiled_parser.common_types import Color @@ -27,3 +28,12 @@ def parse_color(color: str) -> Color: ) raise ValueError("Improperly formatted color passed to parse_color") + + +def check_format(file_path: Path) -> str: + with open(file_path) as file: + line = file.readline().rstrip().strip() + if line[0] == "<": + return "tmx" + else: + return "json" From 5763e8e4972f4077ca09b41df89cf0bbd24f6cf2 Mon Sep 17 00:00:00 2001 From: Darren Eberly Date: Sun, 19 Dec 2021 22:11:18 -0500 Subject: [PATCH 07/13] Layer fix for TMX parser --- pytiled_parser/parsers/tmx/layer.py | 36 ++++++++++++++++++----------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/pytiled_parser/parsers/tmx/layer.py b/pytiled_parser/parsers/tmx/layer.py index 3c4f0b1e..4ba1fa19 100644 --- a/pytiled_parser/parsers/tmx/layer.py +++ b/pytiled_parser/parsers/tmx/layer.py @@ -121,7 +121,7 @@ def _parse_chunk( ) else: data = _convert_raw_tile_layer_data( - [int(v.strip) for v in raw_chunk.text], # type: ignore + [int(v.strip()) for v in raw_chunk.text.split(",")], # type: ignore int(raw_chunk.attrib["width"]), ) @@ -277,23 +277,20 @@ def _parse_image_layer(raw_layer: etree.Element) -> ImageLayer: ImageLayer: The ImageLayer created from raw_layer """ image_element = raw_layer.find("./image") - if image_element: + if image_element is not None: source = Path(image_element.attrib["source"]) - width = int(image_element.attrib["width"]) - height = int(image_element.attrib["height"]) transparent_color = None if image_element.attrib.get("trans") is not None: transparent_color = parse_color(image_element.attrib["trans"]) - common = _parse_common(raw_layer).__dict__ - del common["size"] - return ImageLayer( + image_layer = ImageLayer( image=source, - size=Size(width, height), transparent_color=transparent_color, **_parse_common(raw_layer).__dict__, ) + print(image_layer.size) + return image_layer raise RuntimeError("Tried to parse an image layer that doesn't have an image!") @@ -309,11 +306,24 @@ def _parse_group_layer( Returns: LayerGroup: The LayerGroup created from raw_layer """ - layers = [] - - for layer in raw_layer.iter(): - if layer.tag in ["layer", "objectgroup", "imagelayer", "group"]: - layers.append(parse(layer, parent_dir=parent_dir)) + layers: List[Layer] = [] + for layer in raw_layer.findall("./layer"): + layers.append(_parse_tile_layer(layer)) + + for layer in raw_layer.findall("./objectgroup"): + layers.append(_parse_object_layer(layer, parent_dir)) + + for layer in raw_layer.findall("./imagelayer"): + layers.append(_parse_image_layer(layer)) + + for layer in raw_layer.findall("./group"): + layers.append(_parse_group_layer(layer, parent_dir)) + # layers = [] + # layers = [ + # parse(child_layer, parent_dir=parent_dir) + # for child_layer in raw_layer.iter() + # if child_layer.tag in ["layer", "objectgroup", "imagelayer", "group"] + # ] return LayerGroup(layers=layers, **_parse_common(raw_layer).__dict__) From 62746b63be5446b7b735d00c04f9eadbde71d45f Mon Sep 17 00:00:00 2001 From: Darren Eberly Date: Sun, 19 Dec 2021 22:12:23 -0500 Subject: [PATCH 08/13] tests: All layer unit tests for TMX parser --- .../layer_tests/all_layer_types/map.tmx | 28 ++++++++ .../layer_tests/all_layer_types/tileset.tsx | 4 ++ tests/test_data/layer_tests/b64/map.tmx | 17 +++++ tests/test_data/layer_tests/b64/tileset.tsx | 4 ++ tests/test_data/layer_tests/b64_gzip/map.tmx | 17 +++++ .../layer_tests/b64_gzip/tileset.tsx | 4 ++ tests/test_data/layer_tests/b64_zlib/map.tmx | 17 +++++ .../layer_tests/b64_zlib/tileset.tsx | 4 ++ .../layer_tests/infinite_map/map.tmx | 35 ++++++++++ .../layer_tests/infinite_map_b64/map.tmx | 14 ++++ tests/test_data/layer_tests/no_layers/map.tmx | 13 ++++ .../layer_tests/no_layers/tileset.tsx | 4 ++ tests/test_layer.py | 66 +++++++++++++++++-- 13 files changed, 221 insertions(+), 6 deletions(-) create mode 100644 tests/test_data/layer_tests/all_layer_types/map.tmx create mode 100644 tests/test_data/layer_tests/all_layer_types/tileset.tsx create mode 100644 tests/test_data/layer_tests/b64/map.tmx create mode 100644 tests/test_data/layer_tests/b64/tileset.tsx create mode 100644 tests/test_data/layer_tests/b64_gzip/map.tmx create mode 100644 tests/test_data/layer_tests/b64_gzip/tileset.tsx create mode 100644 tests/test_data/layer_tests/b64_zlib/map.tmx create mode 100644 tests/test_data/layer_tests/b64_zlib/tileset.tsx create mode 100644 tests/test_data/layer_tests/infinite_map/map.tmx create mode 100644 tests/test_data/layer_tests/infinite_map_b64/map.tmx create mode 100644 tests/test_data/layer_tests/no_layers/map.tmx create mode 100644 tests/test_data/layer_tests/no_layers/tileset.tsx diff --git a/tests/test_data/layer_tests/all_layer_types/map.tmx b/tests/test_data/layer_tests/all_layer_types/map.tmx new file mode 100644 index 00000000..c94d181f --- /dev/null +++ b/tests/test_data/layer_tests/all_layer_types/map.tmx @@ -0,0 +1,28 @@ + + + + + + + + +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 + + + + + + + + + + + + + + diff --git a/tests/test_data/layer_tests/all_layer_types/tileset.tsx b/tests/test_data/layer_tests/all_layer_types/tileset.tsx new file mode 100644 index 00000000..8aee17a7 --- /dev/null +++ b/tests/test_data/layer_tests/all_layer_types/tileset.tsx @@ -0,0 +1,4 @@ + + + + diff --git a/tests/test_data/layer_tests/b64/map.tmx b/tests/test_data/layer_tests/b64/map.tmx new file mode 100644 index 00000000..060ebe12 --- /dev/null +++ b/tests/test_data/layer_tests/b64/map.tmx @@ -0,0 +1,17 @@ + + + + + + AQAAAAIAAAADAAAABAAAAAUAAAAGAAAABwAAAAgAAAAJAAAACgAAAAsAAAAMAAAADQAAAA4AAAAPAAAAEAAAABEAAAASAAAAEwAAABQAAAAVAAAAFgAAABcAAAAYAAAAGQAAABoAAAAbAAAAHAAAAB0AAAAeAAAAHwAAACAAAAAhAAAAIgAAACMAAAAkAAAAJQAAACYAAAAnAAAAKAAAACkAAAAqAAAAKwAAACwAAAAtAAAALgAAAC8AAAAwAAAA + + + + + + + + + + + diff --git a/tests/test_data/layer_tests/b64/tileset.tsx b/tests/test_data/layer_tests/b64/tileset.tsx new file mode 100644 index 00000000..8aee17a7 --- /dev/null +++ b/tests/test_data/layer_tests/b64/tileset.tsx @@ -0,0 +1,4 @@ + + + + diff --git a/tests/test_data/layer_tests/b64_gzip/map.tmx b/tests/test_data/layer_tests/b64_gzip/map.tmx new file mode 100644 index 00000000..21316ba3 --- /dev/null +++ b/tests/test_data/layer_tests/b64_gzip/map.tmx @@ -0,0 +1,17 @@ + + + + + + H4sIAAAAAAAACg3DBRKCQAAAwDMRA7BQLMTE9v+vY3dmWyGEth279uwbOTB26MixExNTM6fOnLtwae7KtYUbt+7ce7D0aOXJsxev3rxb+/Dpy7cfv/782wAcvDirwAAAAA== + + + + + + + + + + + diff --git a/tests/test_data/layer_tests/b64_gzip/tileset.tsx b/tests/test_data/layer_tests/b64_gzip/tileset.tsx new file mode 100644 index 00000000..8aee17a7 --- /dev/null +++ b/tests/test_data/layer_tests/b64_gzip/tileset.tsx @@ -0,0 +1,4 @@ + + + + diff --git a/tests/test_data/layer_tests/b64_zlib/map.tmx b/tests/test_data/layer_tests/b64_zlib/map.tmx new file mode 100644 index 00000000..343bd2b3 --- /dev/null +++ b/tests/test_data/layer_tests/b64_zlib/map.tmx @@ -0,0 +1,17 @@ + + + + + + eJwNwwUSgkAAAMAzEQOwUCzExPb/r2N3ZlshhLYdu/bsGzkwdujIsRMTUzOnzpy7cGnuyrWFG7fu3Huw9GjlybMXr968W/vw6cu3H7/+/NsAMw8EmQ== + + + + + + + + + + + diff --git a/tests/test_data/layer_tests/b64_zlib/tileset.tsx b/tests/test_data/layer_tests/b64_zlib/tileset.tsx new file mode 100644 index 00000000..8aee17a7 --- /dev/null +++ b/tests/test_data/layer_tests/b64_zlib/tileset.tsx @@ -0,0 +1,4 @@ + + + + diff --git a/tests/test_data/layer_tests/infinite_map/map.tmx b/tests/test_data/layer_tests/infinite_map/map.tmx new file mode 100644 index 00000000..2a826bbe --- /dev/null +++ b/tests/test_data/layer_tests/infinite_map/map.tmx @@ -0,0 +1,35 @@ + + + + + + + + + + + + + +1,2,3,4, +9,10,11,12, +17,18,19,20, +25,26,27,28, +33,34,35,36, +41,42,43,44, +0,0,0,0, +0,0,0,0 + + +5,6,7,8, +13,14,15,16, +21,22,23,24, +29,30,31,32, +37,38,39,40, +45,46,47,48, +0,0,0,0, +0,0,0,0 + + + + diff --git a/tests/test_data/layer_tests/infinite_map_b64/map.tmx b/tests/test_data/layer_tests/infinite_map_b64/map.tmx new file mode 100644 index 00000000..d83d427b --- /dev/null +++ b/tests/test_data/layer_tests/infinite_map_b64/map.tmx @@ -0,0 +1,14 @@ + + + + + + + + + + AQAAAAIAAAADAAAABAAAAAUAAAAGAAAABwAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAkAAAAKAAAACwAAAAwAAAANAAAADgAAAA8AAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARAAAAEgAAABMAAAAUAAAAFQAAABYAAAAXAAAAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGQAAABoAAAAbAAAAHAAAAB0AAAAeAAAAHwAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACEAAAAiAAAAIwAAACQAAAAlAAAAJgAAACcAAAAoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAApAAAAKgAAACsAAAAsAAAALQAAAC4AAAAvAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA== + + + + diff --git a/tests/test_data/layer_tests/no_layers/map.tmx b/tests/test_data/layer_tests/no_layers/map.tmx new file mode 100644 index 00000000..fb995a34 --- /dev/null +++ b/tests/test_data/layer_tests/no_layers/map.tmx @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/tests/test_data/layer_tests/no_layers/tileset.tsx b/tests/test_data/layer_tests/no_layers/tileset.tsx new file mode 100644 index 00000000..8aee17a7 --- /dev/null +++ b/tests/test_data/layer_tests/no_layers/tileset.tsx @@ -0,0 +1,4 @@ + + + + diff --git a/tests/test_layer.py b/tests/test_layer.py index 75cb733e..bce74b32 100644 --- a/tests/test_layer.py +++ b/tests/test_layer.py @@ -2,11 +2,14 @@ import importlib.util import json import os +import xml.etree.ElementTree as etree from pathlib import Path import pytest -from pytiled_parser.parsers.json.layer import parse +from pytiled_parser.common_types import OrderedPair, Size +from pytiled_parser.parsers.json.layer import parse as parse_json +from pytiled_parser.parsers.tmx.layer import parse as parse_tmx TESTS_DIR = Path(os.path.dirname(os.path.abspath(__file__))) TEST_DATA = TESTS_DIR / "test_data" @@ -25,8 +28,36 @@ ] +def fix_object(my_object): + my_object.coordinates = OrderedPair( + round(my_object.coordinates[0], 4), + round(my_object.coordinates[1], 4), + ) + my_object.size = Size(round(my_object.size[0], 4), round(my_object.size[1], 4)) + + +def fix_layer(layer): + layer.offset = OrderedPair(round(layer.offset[0], 3), round(layer.offset[1], 3)) + layer.coordinates = OrderedPair( + round(layer.coordinates[0], 4), round(layer.coordinates[1], 4) + ) + if layer.size: + layer.size = Size(round(layer.size[0], 4), round(layer.size[1], 4)) + layer.parallax_factor = OrderedPair( + round(layer.parallax_factor[0], 4), + round(layer.parallax_factor[1], 4), + ) + if hasattr(layer, "tiled_objects"): + for tiled_object in layer.tiled_objects: + fix_object(tiled_object) + if hasattr(layer, "layers"): + for child_layer in layer.layers: + fix_layer(child_layer) + + +@pytest.mark.parametrize("parser_type", ["json", "tmx"]) @pytest.mark.parametrize("layer_test", ALL_LAYER_TESTS) -def test_layer_integration(layer_test): +def test_layer_integration(parser_type, layer_test): # it's a PITA to import like this, don't do it # https://stackoverflow.com/a/67692/1342874 spec = importlib.util.spec_from_file_location( @@ -35,10 +66,33 @@ def test_layer_integration(layer_test): expected = importlib.util.module_from_spec(spec) spec.loader.exec_module(expected) - raw_layers_path = layer_test / "map.json" + if parser_type == "json": + raw_layers_path = layer_test / "map.json" + with open(raw_layers_path) as raw_layers_file: + raw_layers = json.load(raw_layers_file)["layers"] + layers = [parse_json(raw_layer) for raw_layer in raw_layers] + elif parser_type == "tmx": + raw_layers_path = layer_test / "map.tmx" + with open(raw_layers_path) as raw_layers_file: + raw_layer = etree.parse(raw_layers_file).getroot() + layers = [] + for layer in raw_layer.findall("./layer"): + layers.append(parse_tmx(layer)) + + for layer in raw_layer.findall("./objectgroup"): + layers.append(parse_tmx(layer)) + + for layer in raw_layer.findall("./group"): + layers.append(parse_tmx(layer)) + + for layer in raw_layer.findall("./imagelayer"): + layers.append(parse_tmx(layer)) + + for layer in layers: + fix_layer(layer) - with open(raw_layers_path) as raw_layers_file: - raw_layers = json.load(raw_layers_file)["layers"] - layers = [parse(raw_layer) for raw_layer in raw_layers] + for layer in expected.EXPECTED: + fix_layer(layer) + print(layer.size) assert layers == expected.EXPECTED From 5b78fd68f1f0e379a7b71c212c4487c6f5f4e2c8 Mon Sep 17 00:00:00 2001 From: Darren Eberly Date: Sun, 19 Dec 2021 22:23:09 -0500 Subject: [PATCH 09/13] tests: All tileset unit tests passing --- tests/test_data/tilesets/image/tileset.tsx | 4 ++ .../image_background_color/tileset.tsx | 4 ++ .../test_data/tilesets/image_grid/tileset.tsx | 8 +++ .../tilesets/image_properties/tileset.tsx | 14 +++++ .../tilesets/image_tile_offset/tileset.tsx | 8 +++ .../image_transformations/tileset.tsx | 5 ++ .../image_transparent_color/tileset.tsx | 4 ++ .../tilesets/individual_images/tileset.tsx | 37 +++++++++++++ tests/test_data/tilesets/terrain/tileset.tsx | 52 +++++++++++++++++++ tests/test_tileset.py | 38 ++++++++++++-- 10 files changed, 169 insertions(+), 5 deletions(-) create mode 100644 tests/test_data/tilesets/image/tileset.tsx create mode 100644 tests/test_data/tilesets/image_background_color/tileset.tsx create mode 100644 tests/test_data/tilesets/image_grid/tileset.tsx create mode 100644 tests/test_data/tilesets/image_properties/tileset.tsx create mode 100644 tests/test_data/tilesets/image_tile_offset/tileset.tsx create mode 100644 tests/test_data/tilesets/image_transformations/tileset.tsx create mode 100644 tests/test_data/tilesets/image_transparent_color/tileset.tsx create mode 100644 tests/test_data/tilesets/individual_images/tileset.tsx create mode 100644 tests/test_data/tilesets/terrain/tileset.tsx diff --git a/tests/test_data/tilesets/image/tileset.tsx b/tests/test_data/tilesets/image/tileset.tsx new file mode 100644 index 00000000..8aee17a7 --- /dev/null +++ b/tests/test_data/tilesets/image/tileset.tsx @@ -0,0 +1,4 @@ + + + + diff --git a/tests/test_data/tilesets/image_background_color/tileset.tsx b/tests/test_data/tilesets/image_background_color/tileset.tsx new file mode 100644 index 00000000..25cae9b0 --- /dev/null +++ b/tests/test_data/tilesets/image_background_color/tileset.tsx @@ -0,0 +1,4 @@ + + + + diff --git a/tests/test_data/tilesets/image_grid/tileset.tsx b/tests/test_data/tilesets/image_grid/tileset.tsx new file mode 100644 index 00000000..62ef87e4 --- /dev/null +++ b/tests/test_data/tilesets/image_grid/tileset.tsx @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/tests/test_data/tilesets/image_properties/tileset.tsx b/tests/test_data/tilesets/image_properties/tileset.tsx new file mode 100644 index 00000000..42478aeb --- /dev/null +++ b/tests/test_data/tilesets/image_properties/tileset.tsx @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/tests/test_data/tilesets/image_tile_offset/tileset.tsx b/tests/test_data/tilesets/image_tile_offset/tileset.tsx new file mode 100644 index 00000000..7c0c1bce --- /dev/null +++ b/tests/test_data/tilesets/image_tile_offset/tileset.tsx @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/tests/test_data/tilesets/image_transformations/tileset.tsx b/tests/test_data/tilesets/image_transformations/tileset.tsx new file mode 100644 index 00000000..5b20f698 --- /dev/null +++ b/tests/test_data/tilesets/image_transformations/tileset.tsx @@ -0,0 +1,5 @@ + + + + + diff --git a/tests/test_data/tilesets/image_transparent_color/tileset.tsx b/tests/test_data/tilesets/image_transparent_color/tileset.tsx new file mode 100644 index 00000000..5ab03467 --- /dev/null +++ b/tests/test_data/tilesets/image_transparent_color/tileset.tsx @@ -0,0 +1,4 @@ + + + + diff --git a/tests/test_data/tilesets/individual_images/tileset.tsx b/tests/test_data/tilesets/individual_images/tileset.tsx new file mode 100644 index 00000000..877cf795 --- /dev/null +++ b/tests/test_data/tilesets/individual_images/tileset.tsx @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/test_data/tilesets/terrain/tileset.tsx b/tests/test_data/tilesets/terrain/tileset.tsx new file mode 100644 index 00000000..2a57297e --- /dev/null +++ b/tests/test_data/tilesets/terrain/tileset.tsx @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/test_tileset.py b/tests/test_tileset.py index c5b8f7ca..9f88f78f 100644 --- a/tests/test_tileset.py +++ b/tests/test_tileset.py @@ -2,11 +2,14 @@ import importlib.util import json import os +import xml.etree.ElementTree as etree from pathlib import Path import pytest -from pytiled_parser.parsers.json.tileset import parse +from pytiled_parser.common_types import OrderedPair, Size +from pytiled_parser.parsers.json.tileset import parse as parse_json +from pytiled_parser.parsers.tmx.tileset import parse as parse_tmx TESTS_DIR = Path(os.path.dirname(os.path.abspath(__file__))) TEST_DATA = TESTS_DIR / "test_data" @@ -26,8 +29,26 @@ ] +def fix_object(my_object): + my_object.coordinates = OrderedPair( + round(my_object.coordinates[0], 4), round(my_object.coordinates[1], 4) + ) + my_object.size = Size(round(my_object.size[0], 4), round(my_object.size[1], 4)) + + +def fix_tileset(tileset): + tileset.version = None + tileset.tiled_version = None + if tileset.tiles: + for tile in tileset.tiles.values(): + if tile.objects: + for my_object in tile.objects.tiled_objects: + fix_object(my_object) + + +@pytest.mark.parametrize("parser_type", ["json", "tmx"]) @pytest.mark.parametrize("tileset_dir", ALL_TILESET_DIRS) -def test_tilesets_integration(tileset_dir): +def test_tilesets_integration(parser_type, tileset_dir): # it's a PITA to import like this, don't do it # https://stackoverflow.com/a/67692/1342874 spec = importlib.util.spec_from_file_location( @@ -36,9 +57,16 @@ def test_tilesets_integration(tileset_dir): expected = importlib.util.module_from_spec(spec) spec.loader.exec_module(expected) - raw_tileset_path = tileset_dir / "tileset.json" + if parser_type == "json": + raw_tileset_path = tileset_dir / "tileset.json" + with open(raw_tileset_path) as raw_tileset: + tileset_ = parse_json(json.loads(raw_tileset.read()), 1) + elif parser_type == "tmx": + raw_tileset_path = tileset_dir / "tileset.tsx" + with open(raw_tileset_path) as raw_tileset: + tileset_ = parse_tmx(etree.parse(raw_tileset).getroot(), 1) - with open(raw_tileset_path) as raw_tileset: - tileset_ = parse(json.loads(raw_tileset.read()), 1) + fix_tileset(tileset_) + fix_tileset(expected.EXPECTED) assert tileset_ == expected.EXPECTED From 22d7631bfb3f5400c605a07951192d8f30911111 Mon Sep 17 00:00:00 2001 From: Darren Eberly Date: Mon, 20 Dec 2021 00:08:50 -0500 Subject: [PATCH 10/13] All object tests passing for TMX --- pytiled_parser/parsers/tmx/tiled_object.py | 68 +-- ...ed_object.py => test_tiled_object_json.py} | 0 tests/test_tiled_object_tmx.py | 492 ++++++++++++++++++ 3 files changed, 529 insertions(+), 31 deletions(-) rename tests/{test_tiled_object.py => test_tiled_object_json.py} (100%) create mode 100644 tests/test_tiled_object_tmx.py diff --git a/pytiled_parser/parsers/tmx/tiled_object.py b/pytiled_parser/parsers/tmx/tiled_object.py index 455c2c5e..9d272ea9 100644 --- a/pytiled_parser/parsers/tmx/tiled_object.py +++ b/pytiled_parser/parsers/tmx/tiled_object.py @@ -123,9 +123,11 @@ def _parse_polyline(raw_object: etree.Element) -> Polyline: Polyline: The Polyline object created from the raw object """ polyline = [] - for raw_point in raw_object.attrib["polyline"].split(" "): - point = raw_point.split(",") - polyline.append(OrderedPair(float(point[0]), float(point[1]))) + polyline_element = raw_object.find("./polyline") + if polyline_element is not None: + for raw_point in polyline_element.attrib["points"].split(" "): + point = raw_point.split(",") + polyline.append(OrderedPair(float(point[0]), float(point[1]))) return Polyline(points=polyline, **_parse_common(raw_object).__dict__) @@ -161,46 +163,50 @@ def _parse_text(raw_object: etree.Element) -> Text: Text: The Text object created from the raw object """ # required attributes - text = raw_object.text + text_element = raw_object.find("./text") + + if text_element is not None: + text = text_element.text + + if not text: + text = "" + # create base Text object + text_object = Text(text=text, **_parse_common(raw_object).__dict__) - if not text: - text = "" - # create base Text object - text_object = Text(text=text, **_parse_common(raw_object).__dict__) + # optional attributes - # optional attributes - if raw_object.attrib.get("color") is not None: - text_object.color = parse_color(raw_object.attrib["color"]) + if text_element.attrib.get("color") is not None: + text_object.color = parse_color(text_element.attrib["color"]) - if raw_object.attrib.get("fontfamily") is not None: - text_object.font_family = raw_object.attrib["fontfamily"] + if text_element.attrib.get("fontfamily") is not None: + text_object.font_family = text_element.attrib["fontfamily"] - if raw_object.attrib.get("pixelsize") is not None: - text_object.font_size = float(raw_object.attrib["pixelsize"]) + if text_element.attrib.get("pixelsize") is not None: + text_object.font_size = float(text_element.attrib["pixelsize"]) - if raw_object.attrib.get("bold") is not None: - text_object.bold = bool(int(raw_object.attrib["bold"])) + if text_element.attrib.get("bold") is not None: + text_object.bold = bool(int(text_element.attrib["bold"])) - if raw_object.attrib.get("italic") is not None: - text_object.italic = bool(int(raw_object.attrib["italic"])) + if text_element.attrib.get("italic") is not None: + text_object.italic = bool(int(text_element.attrib["italic"])) - if raw_object.attrib.get("kerning") is not None: - text_object.kerning = bool(int(raw_object.attrib["kerning"])) + if text_element.attrib.get("kerning") is not None: + text_object.kerning = bool(int(text_element.attrib["kerning"])) - if raw_object.attrib.get("strikeout") is not None: - text_object.strike_out = bool(int(raw_object.attrib["strikeout"])) + if text_element.attrib.get("strikeout") is not None: + text_object.strike_out = bool(int(text_element.attrib["strikeout"])) - if raw_object.attrib.get("underline") is not None: - text_object.underline = bool(int(raw_object.attrib["underline"])) + if text_element.attrib.get("underline") is not None: + text_object.underline = bool(int(text_element.attrib["underline"])) - if raw_object.attrib.get("halign") is not None: - text_object.horizontal_align = raw_object.attrib["halign"] + if text_element.attrib.get("halign") is not None: + text_object.horizontal_align = text_element.attrib["halign"] - if raw_object.attrib.get("valign") is not None: - text_object.vertical_align = raw_object.attrib["valign"] + if text_element.attrib.get("valign") is not None: + text_object.vertical_align = text_element.attrib["valign"] - if raw_object.attrib.get("wrap") is not None: - text_object.wrap = bool(int(raw_object.attrib["wrap"])) + if text_element.attrib.get("wrap") is not None: + text_object.wrap = bool(int(text_element.attrib["wrap"])) return text_object diff --git a/tests/test_tiled_object.py b/tests/test_tiled_object_json.py similarity index 100% rename from tests/test_tiled_object.py rename to tests/test_tiled_object_json.py diff --git a/tests/test_tiled_object_tmx.py b/tests/test_tiled_object_tmx.py new file mode 100644 index 00000000..d2777594 --- /dev/null +++ b/tests/test_tiled_object_tmx.py @@ -0,0 +1,492 @@ +"""Tests for objects""" +import xml.etree.ElementTree as etree +from contextlib import ExitStack as does_not_raise +from pathlib import Path + +import pytest + +from pytiled_parser import common_types +from pytiled_parser.parsers.tmx.tiled_object import parse +from pytiled_parser.tiled_object import ( + Ellipse, + Point, + Polygon, + Polyline, + Rectangle, + Text, + Tile, +) + +ELLIPSES = [ + ( + """ + + + + """, + Ellipse( + id=6, + size=common_types.Size(57.4014, 18.5518), + name="ellipse", + coordinates=common_types.OrderedPair(37.5401, 81.1913), + ), + ), + ( + """ + + + + """, + Ellipse( + id=7, + size=common_types.Size(6.3294, 31.4289), + name="ellipse - invisible", + visible=False, + coordinates=common_types.OrderedPair(22.6986, 53.9093), + ), + ), + ( + """ + + + + """, + Ellipse( + id=8, + size=common_types.Size(29.6828, 24.2264), + name="ellipse - rotated", + rotation=111, + coordinates=common_types.OrderedPair(35.7940, 120.0409), + ), + ), + ( + """ + + + + """, + Ellipse( + id=29, + name="ellipse - no width or height", + coordinates=common_types.OrderedPair(72.4611, 127.6799), + ), + ), +] + +RECTANGLES = [ + ( + """ + + """, + Rectangle( + id=1, + size=common_types.Size(45.3973, 41.4687), + coordinates=common_types.OrderedPair(27.7185, 23.5717), + name="rectangle", + ), + ), + ( + """ + + """, + Rectangle( + id=4, + size=common_types.Size(30.9924, 32.7384), + coordinates=common_types.OrderedPair(163.9104, 91.0128), + name="rectangle - invisible", + visible=False, + ), + ), + ( + """ + + """, + Rectangle( + id=5, + size=common_types.Size(10, 22), + coordinates=common_types.OrderedPair(183.3352, 23.3534), + name="rectangle - rotated", + rotation=10, + ), + ), + ( + """ + + """, + Rectangle( + id=28, + coordinates=common_types.OrderedPair(131.1720, 53.4728), + name="rectangle - no width or height", + ), + ), + ( + r""" + + + + + + + + + + + """, + Rectangle( + id=30, + size=common_types.Size(21.1709, 13.7501), + coordinates=common_types.OrderedPair(39.0679, 131.8268), + name="rectangle - properties", + properties={ + "bool property": False, + "color property": common_types.Color(170, 0, 0, 255), + "file property": Path("../../../../../../dev/null"), + "float property": 42.1, + "int property": 8675309, + "string property": "pytiled_parser rulez!1!!", + }, + ), + ), +] + +POINTS = [ + ( + """ + + + + """, + Point( + id=2, coordinates=common_types.OrderedPair(159.9818, 82.9374), name="point" + ), + ), + ( + """ + + + + """, + Point( + id=2, + coordinates=common_types.OrderedPair(159.9818, 82.9374), + name="point - invisible", + visible=False, + ), + ), +] + +POLYGONS = [ + ( + """ + + + + """, + Polygon( + id=9, + coordinates=common_types.OrderedPair(89.4851, 38.6314), + name="polygon", + points=[ + common_types.OrderedPair(0, 0), + common_types.OrderedPair(19.4248, 27.0638), + common_types.OrderedPair(19.6431, 3.0556), + common_types.OrderedPair(-2.6191, 15.9327), + common_types.OrderedPair(25.3177, 16.3692), + ], + ), + ), + ( + """ + + + + """, + Polygon( + id=9, + coordinates=common_types.OrderedPair(89.4851, 38.6314), + name="polygon - invisible", + points=[ + common_types.OrderedPair(0, 0), + common_types.OrderedPair(19.4248, 27.0638), + common_types.OrderedPair(19.6431, 3.0556), + common_types.OrderedPair(-2.6191, 15.9327), + common_types.OrderedPair(25.3177, 16.3692), + ], + visible=False, + ), + ), + ( + """ + + + + """, + Polygon( + id=9, + coordinates=common_types.OrderedPair(89.4851, 38.6314), + name="polygon - rotated", + points=[ + common_types.OrderedPair(0, 0), + common_types.OrderedPair(19.4248, 27.0638), + common_types.OrderedPair(19.6431, 3.0556), + common_types.OrderedPair(-2.6191, 15.9327), + common_types.OrderedPair(25.3177, 16.3692), + ], + rotation=123, + ), + ), +] + +POLYLINES = [ + ( + """ + + + + """, + Polyline( + id=12, + coordinates=common_types.OrderedPair(124.1878, 90.1398), + name="polyline", + points=[ + common_types.OrderedPair(0, 0), + common_types.OrderedPair(-13.3136, 41.0321), + common_types.OrderedPair(21.3891, 16.8057), + ], + ), + ), + ( + """ + + + + """, + Polyline( + id=12, + coordinates=common_types.OrderedPair(124.1878, 90.1398), + name="polyline - invisible", + points=[ + common_types.OrderedPair(0, 0), + common_types.OrderedPair(-13.3136, 41.0321), + common_types.OrderedPair(21.3891, 16.8057), + ], + visible=False, + ), + ), + ( + """ + + + + """, + Polyline( + id=12, + coordinates=common_types.OrderedPair(124.1878, 90.1398), + name="polyline - rotated", + points=[ + common_types.OrderedPair(0, 0), + common_types.OrderedPair(-13.3136, 41.0321), + common_types.OrderedPair(21.3891, 16.8057), + ], + rotation=110, + ), + ), +] + +TEXTS = [ + ( + """ + + Hello World + + """, + Text( + id=19, + name="text", + text="Hello World", + size=common_types.Size(92.375, 19), + coordinates=common_types.OrderedPair(93.2987, 81.7106), + ), + ), + ( + """ + + Hello World + + """, + Text( + id=19, + name="text - wrap", + text="Hello World", + wrap=True, + size=common_types.Size(92.375, 19), + coordinates=common_types.OrderedPair(93.2987, 81.7106), + ), + ), + ( + """ + + Hello World + + """, + Text( + id=19, + name="text - rotated", + text="Hello World", + rotation=110, + size=common_types.Size(92.375, 19), + coordinates=common_types.OrderedPair(93.2987, 81.7106), + ), + ), + ( + """ + + Hello World + + """, + Text( + id=19, + name="text - different font", + text="Hello World", + font_size=19, + font_family="DejaVu Sans", + rotation=110, + size=common_types.Size(92.375, 19), + coordinates=common_types.OrderedPair(93.2987, 81.7106), + ), + ), + ( + """ + + Hello World + + """, + Text( + id=19, + name="text - right bottom align", + text="Hello World", + horizontal_align="right", + vertical_align="bottom", + size=common_types.Size(92.375, 19), + coordinates=common_types.OrderedPair(93.2987, 81.7106), + ), + ), + ( + """ + + Hello World + + """, + Text( + id=19, + name="text - center center align", + text="Hello World", + horizontal_align="center", + vertical_align="center", + size=common_types.Size(92.375, 19), + coordinates=common_types.OrderedPair(93.2987, 81.7106), + ), + ), + ( + """ + + Hello World + + """, + Text( + id=19, + name="text - justified", + text="Hello World", + horizontal_align="justify", + size=common_types.Size(92.375, 19), + coordinates=common_types.OrderedPair(93.2987, 81.7106), + ), + ), + ( + """ + + Hello World + + """, + Text( + id=19, + name="text - colored", + text="Hello World", + color=common_types.Color(170, 0, 0, 255), + size=common_types.Size(92.375, 19), + coordinates=common_types.OrderedPair(93.2987, 81.7106), + ), + ), + ( + """ + + Hello World + + """, + Text( + id=19, + name="text - font options", + text="Hello World", + size=common_types.Size(92.375, 19), + bold=True, + italic=True, + kerning=True, + strike_out=True, + underline=True, + wrap=True, + coordinates=common_types.OrderedPair(93.2987, 81.7106), + ), + ), +] + +TILES = [ + ( + """ + + """, + Tile( + id=13, + size=common_types.Size(32, 32), + name="tile", + coordinates=common_types.OrderedPair(111.8981, 48.3019), + gid=79, + ), + ), + ( + """ + + """, + Tile( + id=13, + size=common_types.Size(32, 32), + name="tile - invisible", + type="tile", + coordinates=common_types.OrderedPair(111.8981, 48.3019), + gid=79, + visible=False, + ), + ), + ( + """ + + """, + Tile( + id=13, + size=common_types.Size(32, 32), + name="tile - rotated", + coordinates=common_types.OrderedPair(111.8981, 48.3019), + gid=79, + rotation=110, + ), + ), +] + +OBJECTS = ELLIPSES + RECTANGLES + POINTS + POLYGONS + POLYLINES + TEXTS + TILES + + +@pytest.mark.parametrize("raw_object_tmx,expected", OBJECTS) +def test_parse_layer(raw_object_tmx, expected): + raw_object = etree.fromstring(raw_object_tmx) + result = parse(raw_object) + + assert result == expected From 923149c0a46d4ed4c7fb37c953f4e77b4b9764e3 Mon Sep 17 00:00:00 2001 From: Darren Eberly Date: Tue, 21 Dec 2021 21:50:32 -0500 Subject: [PATCH 11/13] Better object template loading --- pytiled_parser/parsers/json/tiled_object.py | 20 +++++----- pytiled_parser/parsers/tmx/tiled_map.py | 1 + pytiled_parser/parsers/tmx/tiled_object.py | 27 ++++++------- pytiled_parser/util.py | 43 +++++++++++++++++++++ 4 files changed, 65 insertions(+), 26 deletions(-) diff --git a/pytiled_parser/parsers/json/tiled_object.py b/pytiled_parser/parsers/json/tiled_object.py index e5376373..2acc4b30 100644 --- a/pytiled_parser/parsers/json/tiled_object.py +++ b/pytiled_parser/parsers/json/tiled_object.py @@ -1,6 +1,7 @@ """Object parsing for the JSON Map Format. """ import json +import xml.etree.ElementTree as etree from pathlib import Path from typing import Any, Callable, Dict, List, Optional @@ -19,7 +20,7 @@ Tile, TiledObject, ) -from pytiled_parser.util import parse_color +from pytiled_parser.util import load_object_template, parse_color class RawText(TypedDict): @@ -300,20 +301,19 @@ def parse( "A parent directory must be specified when using object templates." ) template_path = Path(parent_dir / raw_object["template"]) - with open(template_path) as raw_template_file: - template = json.load(raw_template_file) - if "tileset" in template: - tileset_path = Path( - template_path.parent / template["tileset"]["source"] - ) - with open(tileset_path) as raw_tileset_file: - new_tileset = json.load(raw_tileset_file) - new_tileset_path = tileset_path.parent + template, new_tileset, new_tileset_path = load_object_template(template_path) + if isinstance(template, dict): loaded_template = template["object"] for key in loaded_template: if key != "id": raw_object[key] = loaded_template[key] # type: ignore + elif isinstance(template, etree.Element): + # load the XML object into the JSON object + raise NotImplementedError( + "Loading TMX object templates inside a JSON map is currently not supported, " + "but will be in a future release." + ) if raw_object.get("gid"): return _parse_tile(raw_object, new_tileset, new_tileset_path) diff --git a/pytiled_parser/parsers/tmx/tiled_map.py b/pytiled_parser/parsers/tmx/tiled_map.py index 4ecb5ed3..f12c6e21 100644 --- a/pytiled_parser/parsers/tmx/tiled_map.py +++ b/pytiled_parser/parsers/tmx/tiled_map.py @@ -94,6 +94,7 @@ def parse(file: Path) -> TiledMap: break if not already_loaded: + print("here") highest_firstgid = max(map_.tilesets.keys()) last_tileset_count = map_.tilesets[highest_firstgid].tile_count new_firstgid = highest_firstgid + last_tileset_count diff --git a/pytiled_parser/parsers/tmx/tiled_object.py b/pytiled_parser/parsers/tmx/tiled_object.py index 9d272ea9..ceb4e08f 100644 --- a/pytiled_parser/parsers/tmx/tiled_object.py +++ b/pytiled_parser/parsers/tmx/tiled_object.py @@ -1,3 +1,4 @@ +import json import xml.etree.ElementTree as etree from pathlib import Path from typing import Callable, Optional @@ -14,7 +15,7 @@ Tile, TiledObject, ) -from pytiled_parser.util import parse_color +from pytiled_parser.util import load_object_template, parse_color def _parse_common(raw_object: etree.Element) -> TiledObject: @@ -264,18 +265,9 @@ def parse(raw_object: etree.Element, parent_dir: Optional[Path] = None) -> Tiled "A parent directory must be specified when using object templates." ) template_path = Path(parent_dir / raw_object.attrib["template"]) - with open(template_path) as template_file: - template = etree.parse(template_file).getroot() - - tileset_element = template.find("./tileset") - if tileset_element: - tileset_path = Path( - template_path.parent / tileset_element.attrib["source"] - ) - with open(tileset_path) as tileset_file: - new_tileset = etree.parse(tileset_file).getroot() - new_tileset_path = tileset_path.parent + template, new_tileset, new_tileset_path = load_object_template(template_path) + if isinstance(template, etree.Element): new_object = template.find("./object") if new_object is not None: if raw_object.attrib.get("id") is not None: @@ -288,11 +280,14 @@ def parse(raw_object: etree.Element, parent_dir: Optional[Path] = None) -> Tiled new_object.attrib["y"] = raw_object.attrib["y"] raw_object = new_object - - if raw_object.attrib.get("gid"): - return _parse_tile(raw_object, new_tileset, new_tileset_path) + elif isinstance(template, dict): + # load the JSON object into the XML object + raise NotImplementedError( + "Loading JSON object templates inside a TMX map is currently not supported, " + "but will be in a future release." + ) if raw_object.attrib.get("gid"): - return _parse_tile(raw_object) + return _parse_tile(raw_object, new_tileset, new_tileset_path) return _get_parser(raw_object)(raw_object) diff --git a/pytiled_parser/util.py b/pytiled_parser/util.py index 07fd9554..f8bb18bf 100644 --- a/pytiled_parser/util.py +++ b/pytiled_parser/util.py @@ -1,5 +1,8 @@ """Utility Functions for PyTiled""" +import json +import xml.etree.ElementTree as etree from pathlib import Path +from typing import Any from pytiled_parser.common_types import Color @@ -37,3 +40,43 @@ def check_format(file_path: Path) -> str: return "tmx" else: return "json" + + +def load_object_template(file_path: Path) -> Any: + template_format = check_format(file_path) + + new_tileset = None + new_tileset_path = None + + if template_format == "tmx": + with open(file_path) as template_file: + template = etree.parse(template_file).getroot() + + tileset_element = template.find("./tileset") + if tileset_element is not None: + tileset_path = Path(file_path.parent / tileset_element.attrib["source"]) + new_tileset = load_object_tileset(tileset_path) + new_tileset_path = tileset_path.parent + elif template_format == "json": + with open(file_path) as template_file: + template = json.load(template_file) + if "tileset" in template: + tileset_path = Path(file_path.parent / template["tileset"]["source"]) # type: ignore + new_tileset = load_object_tileset(tileset_path) + new_tileset_path = tileset_path.parent + + return (template, new_tileset, new_tileset_path) + + +def load_object_tileset(file_path: Path) -> Any: + tileset_format = check_format(file_path) + + new_tileset = None + + with open(file_path) as tileset_file: + if tileset_format == "tmx": + new_tileset = etree.parse(tileset_file).getroot() + elif tileset_format == "json": + new_tileset = json.load(tileset_file) + + return new_tileset From 72116adce6d9ef68ec79b0a1d0662f049d06c2f5 Mon Sep 17 00:00:00 2001 From: Darren Eberly Date: Tue, 21 Dec 2021 21:50:42 -0500 Subject: [PATCH 12/13] tests: All map tests working --- tests/test_cross_template/map.json | 73 +++++++++++++++++++ tests/test_cross_template/map.tmx | 16 ++++ .../template-rectangle.json | 12 +++ .../test_cross_template/template-rectangle.tx | 4 + .../test_cross_template.py | 16 ++++ .../tile_set_image_for_template.json | 14 ++++ .../tile_set_image_for_template.tsx | 4 + tests/test_cross_template/tileset.json | 14 ++++ tests/test_cross_template/tileset.tsx | 4 + .../map_tests/embedded_tileset/map.tmx | 6 ++ .../external_tileset_dif_dir/map.tmx | 17 +++++ .../tileset/tileset.tsx | 37 ++++++++++ tests/test_data/map_tests/hexagonal/map.tmx | 18 +++++ .../test_data/map_tests/hexagonal/tileset.tsx | 5 ++ .../map_tests/no_background_color/map.tmx | 4 + .../map_tests/no_background_color/tileset.tsx | 4 + tests/test_data/map_tests/no_layers/map.tmx | 12 +++ .../test_data/map_tests/no_layers/tileset.tsx | 4 + tests/test_data/map_tests/template/map.tmx | 18 +++++ .../map_tests/template/template-rectangle.tx | 4 + .../map_tests/template/template-tile-image.tx | 5 ++ .../template/template-tile-spritesheet.tx | 5 ++ .../template/tile_set_image_for_template.tsx | 4 + .../template/tile_set_single_image.tsx | 7 ++ .../test_data/map_tests/template/tileset.tsx | 4 + tests/test_map.py | 52 ++++++++++++- 26 files changed, 361 insertions(+), 2 deletions(-) create mode 100644 tests/test_cross_template/map.json create mode 100644 tests/test_cross_template/map.tmx create mode 100644 tests/test_cross_template/template-rectangle.json create mode 100644 tests/test_cross_template/template-rectangle.tx create mode 100644 tests/test_cross_template/test_cross_template.py create mode 100644 tests/test_cross_template/tile_set_image_for_template.json create mode 100644 tests/test_cross_template/tile_set_image_for_template.tsx create mode 100644 tests/test_cross_template/tileset.json create mode 100644 tests/test_cross_template/tileset.tsx create mode 100644 tests/test_data/map_tests/embedded_tileset/map.tmx create mode 100644 tests/test_data/map_tests/external_tileset_dif_dir/map.tmx create mode 100644 tests/test_data/map_tests/external_tileset_dif_dir/tileset/tileset.tsx create mode 100644 tests/test_data/map_tests/hexagonal/map.tmx create mode 100644 tests/test_data/map_tests/hexagonal/tileset.tsx create mode 100644 tests/test_data/map_tests/no_background_color/map.tmx create mode 100644 tests/test_data/map_tests/no_background_color/tileset.tsx create mode 100644 tests/test_data/map_tests/no_layers/map.tmx create mode 100644 tests/test_data/map_tests/no_layers/tileset.tsx create mode 100644 tests/test_data/map_tests/template/map.tmx create mode 100644 tests/test_data/map_tests/template/template-rectangle.tx create mode 100644 tests/test_data/map_tests/template/template-tile-image.tx create mode 100644 tests/test_data/map_tests/template/template-tile-spritesheet.tx create mode 100644 tests/test_data/map_tests/template/tile_set_image_for_template.tsx create mode 100644 tests/test_data/map_tests/template/tile_set_single_image.tsx create mode 100644 tests/test_data/map_tests/template/tileset.tsx diff --git a/tests/test_cross_template/map.json b/tests/test_cross_template/map.json new file mode 100644 index 00000000..451323f0 --- /dev/null +++ b/tests/test_cross_template/map.json @@ -0,0 +1,73 @@ +{ "backgroundcolor":"#ff0004", + "compressionlevel":0, + "height":6, + "infinite":false, + "layers":[ + { + "draworder":"topdown", + "id":2, + "name":"Object Layer 1", + "objects":[ + { + "id":2, + "template":"template-rectangle.tx", + "x":98.4987608686521, + "y":46.2385012811358 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":3, + "nextobjectid":8, + "orientation":"orthogonal", + "properties":[ + { + "name":"bool property - true", + "type":"bool", + "value":true + }, + { + "name":"color property", + "type":"color", + "value":"#ff49fcff" + }, + { + "name":"file property", + "type":"file", + "value":"..\/..\/..\/..\/..\/..\/var\/log\/syslog" + }, + { + "name":"float property", + "type":"float", + "value":1.23456789 + }, + { + "name":"int property", + "type":"int", + "value":13 + }, + { + "name":"string property", + "type":"string", + "value":"Hello, World!!" + }], + "renderorder":"right-down", + "tiledversion":"1.7.1", + "tileheight":32, + "tilesets":[ + { + "firstgid":1, + "source":"tileset.json" + }, + { + "firstgid":49, + "source":"tile_set_image_for_template.json" + }], + "tilewidth":32, + "type":"map", + "version":"1.6", + "width":8 +} \ No newline at end of file diff --git a/tests/test_cross_template/map.tmx b/tests/test_cross_template/map.tmx new file mode 100644 index 00000000..b77d63b4 --- /dev/null +++ b/tests/test_cross_template/map.tmx @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/tests/test_cross_template/template-rectangle.json b/tests/test_cross_template/template-rectangle.json new file mode 100644 index 00000000..fc392298 --- /dev/null +++ b/tests/test_cross_template/template-rectangle.json @@ -0,0 +1,12 @@ +{ "object": + { + "height":38.2811778048473, + "id":1, + "name":"", + "rotation":0, + "type":"", + "visible":true, + "width":63.6585878103079 + }, + "type":"template" +} \ No newline at end of file diff --git a/tests/test_cross_template/template-rectangle.tx b/tests/test_cross_template/template-rectangle.tx new file mode 100644 index 00000000..6daa3644 --- /dev/null +++ b/tests/test_cross_template/template-rectangle.tx @@ -0,0 +1,4 @@ + + diff --git a/tests/test_cross_template/test_cross_template.py b/tests/test_cross_template/test_cross_template.py new file mode 100644 index 00000000..a1f8d86a --- /dev/null +++ b/tests/test_cross_template/test_cross_template.py @@ -0,0 +1,16 @@ +import os +from pathlib import Path + +import pytest + +from pytiled_parser import parse_map + + +def test_cross_template_tmx_json(): + with pytest.raises(NotImplementedError): + parse_map(Path(os.path.dirname(os.path.abspath(__file__))) / "map.tmx") + + +def test_cross_template_json_tmx(): + with pytest.raises(NotImplementedError): + parse_map(Path(os.path.dirname(os.path.abspath(__file__))) / "map.json") diff --git a/tests/test_cross_template/tile_set_image_for_template.json b/tests/test_cross_template/tile_set_image_for_template.json new file mode 100644 index 00000000..c0cbf4e2 --- /dev/null +++ b/tests/test_cross_template/tile_set_image_for_template.json @@ -0,0 +1,14 @@ +{ "columns":1, + "image":"..\/..\/images\/tile_04.png", + "imageheight":32, + "imagewidth":32, + "margin":0, + "name":"tile_set_image_for_template", + "spacing":0, + "tilecount":1, + "tiledversion":"1.7.1", + "tileheight":32, + "tilewidth":32, + "type":"tileset", + "version":"1.6" +} \ No newline at end of file diff --git a/tests/test_cross_template/tile_set_image_for_template.tsx b/tests/test_cross_template/tile_set_image_for_template.tsx new file mode 100644 index 00000000..9c597790 --- /dev/null +++ b/tests/test_cross_template/tile_set_image_for_template.tsx @@ -0,0 +1,4 @@ + + + + diff --git a/tests/test_cross_template/tileset.json b/tests/test_cross_template/tileset.json new file mode 100644 index 00000000..33028848 --- /dev/null +++ b/tests/test_cross_template/tileset.json @@ -0,0 +1,14 @@ +{ "columns":8, + "image":"..\/test_data\/images\/tmw_desert_spacing.png", + "imageheight":199, + "imagewidth":265, + "margin":1, + "name":"tile_set_image", + "spacing":1, + "tilecount":48, + "tiledversion":"1.6.0", + "tileheight":32, + "tilewidth":32, + "type":"tileset", + "version":"1.6" +} \ No newline at end of file diff --git a/tests/test_cross_template/tileset.tsx b/tests/test_cross_template/tileset.tsx new file mode 100644 index 00000000..8aee17a7 --- /dev/null +++ b/tests/test_cross_template/tileset.tsx @@ -0,0 +1,4 @@ + + + + diff --git a/tests/test_data/map_tests/embedded_tileset/map.tmx b/tests/test_data/map_tests/embedded_tileset/map.tmx new file mode 100644 index 00000000..c39ca67b --- /dev/null +++ b/tests/test_data/map_tests/embedded_tileset/map.tmx @@ -0,0 +1,6 @@ + + + + + + diff --git a/tests/test_data/map_tests/external_tileset_dif_dir/map.tmx b/tests/test_data/map_tests/external_tileset_dif_dir/map.tmx new file mode 100644 index 00000000..9c19e157 --- /dev/null +++ b/tests/test_data/map_tests/external_tileset_dif_dir/map.tmx @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + eAFjYWBgYAZiJiBmBOKhBgAIGAAL + + + diff --git a/tests/test_data/map_tests/external_tileset_dif_dir/tileset/tileset.tsx b/tests/test_data/map_tests/external_tileset_dif_dir/tileset/tileset.tsx new file mode 100644 index 00000000..192b15e3 --- /dev/null +++ b/tests/test_data/map_tests/external_tileset_dif_dir/tileset/tileset.tsx @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/test_data/map_tests/hexagonal/map.tmx b/tests/test_data/map_tests/hexagonal/map.tmx new file mode 100644 index 00000000..482cf3cf --- /dev/null +++ b/tests/test_data/map_tests/hexagonal/map.tmx @@ -0,0 +1,18 @@ + + + + + +3,3,3,3,9,9,9,9,17,17, +3,3,3,9,9,9,9,17,17,17, +3,3,3,9,9,9,9,9,17,17, +3,3,1,7,9,9,9,15,17,17, +1,1,12,5,7,7,7,15,15,15, +12,1,5,5,7,7,7,15,15,15, +2,2,5,5,5,5,4,14,14,14, +2,2,5,5,5,4,14,14,14,14, +2,2,2,5,5,5,4,14,14,14, +2,2,2,2,5,5,4,4,14,14 + + + diff --git a/tests/test_data/map_tests/hexagonal/tileset.tsx b/tests/test_data/map_tests/hexagonal/tileset.tsx new file mode 100644 index 00000000..cba4d04e --- /dev/null +++ b/tests/test_data/map_tests/hexagonal/tileset.tsx @@ -0,0 +1,5 @@ + + + + + diff --git a/tests/test_data/map_tests/no_background_color/map.tmx b/tests/test_data/map_tests/no_background_color/map.tmx new file mode 100644 index 00000000..7e71558c --- /dev/null +++ b/tests/test_data/map_tests/no_background_color/map.tmx @@ -0,0 +1,4 @@ + + + + diff --git a/tests/test_data/map_tests/no_background_color/tileset.tsx b/tests/test_data/map_tests/no_background_color/tileset.tsx new file mode 100644 index 00000000..8b1cf24b --- /dev/null +++ b/tests/test_data/map_tests/no_background_color/tileset.tsx @@ -0,0 +1,4 @@ + + + + diff --git a/tests/test_data/map_tests/no_layers/map.tmx b/tests/test_data/map_tests/no_layers/map.tmx new file mode 100644 index 00000000..1ef8b7a0 --- /dev/null +++ b/tests/test_data/map_tests/no_layers/map.tmx @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/tests/test_data/map_tests/no_layers/tileset.tsx b/tests/test_data/map_tests/no_layers/tileset.tsx new file mode 100644 index 00000000..8b1cf24b --- /dev/null +++ b/tests/test_data/map_tests/no_layers/tileset.tsx @@ -0,0 +1,4 @@ + + + + diff --git a/tests/test_data/map_tests/template/map.tmx b/tests/test_data/map_tests/template/map.tmx new file mode 100644 index 00000000..24cc2f0d --- /dev/null +++ b/tests/test_data/map_tests/template/map.tmx @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/tests/test_data/map_tests/template/template-rectangle.tx b/tests/test_data/map_tests/template/template-rectangle.tx new file mode 100644 index 00000000..6daa3644 --- /dev/null +++ b/tests/test_data/map_tests/template/template-rectangle.tx @@ -0,0 +1,4 @@ + + diff --git a/tests/test_data/map_tests/template/template-tile-image.tx b/tests/test_data/map_tests/template/template-tile-image.tx new file mode 100644 index 00000000..989b725d --- /dev/null +++ b/tests/test_data/map_tests/template/template-tile-image.tx @@ -0,0 +1,5 @@ + + diff --git a/tests/test_data/map_tests/template/template-tile-spritesheet.tx b/tests/test_data/map_tests/template/template-tile-spritesheet.tx new file mode 100644 index 00000000..d958c770 --- /dev/null +++ b/tests/test_data/map_tests/template/template-tile-spritesheet.tx @@ -0,0 +1,5 @@ + + diff --git a/tests/test_data/map_tests/template/tile_set_image_for_template.tsx b/tests/test_data/map_tests/template/tile_set_image_for_template.tsx new file mode 100644 index 00000000..9c597790 --- /dev/null +++ b/tests/test_data/map_tests/template/tile_set_image_for_template.tsx @@ -0,0 +1,4 @@ + + + + diff --git a/tests/test_data/map_tests/template/tile_set_single_image.tsx b/tests/test_data/map_tests/template/tile_set_single_image.tsx new file mode 100644 index 00000000..c881c112 --- /dev/null +++ b/tests/test_data/map_tests/template/tile_set_single_image.tsx @@ -0,0 +1,7 @@ + + + + + + + diff --git a/tests/test_data/map_tests/template/tileset.tsx b/tests/test_data/map_tests/template/tileset.tsx new file mode 100644 index 00000000..8aee17a7 --- /dev/null +++ b/tests/test_data/map_tests/template/tileset.tsx @@ -0,0 +1,4 @@ + + + + diff --git a/tests/test_map.py b/tests/test_map.py index 68ad9967..424e802c 100644 --- a/tests/test_map.py +++ b/tests/test_map.py @@ -6,6 +6,7 @@ import pytest from pytiled_parser import parse_map +from pytiled_parser.common_types import OrderedPair, Size TESTS_DIR = Path(os.path.dirname(os.path.abspath(__file__))) TEST_DATA = TESTS_DIR / "test_data" @@ -21,17 +22,64 @@ ] +def fix_object(my_object): + my_object.coordinates = OrderedPair( + round(my_object.coordinates[0], 3), round(my_object.coordinates[1], 3) + ) + my_object.size = Size(round(my_object.size[0], 4), round(my_object.size[1], 4)) + + +def fix_tileset(tileset): + tileset.version = None + tileset.tiled_version = None + if tileset.tiles: + for tile in tileset.tiles.values(): + if tile.objects: + for my_object in tile.objects.tiled_objects: + fix_object(my_object) + + +def fix_layer(layer): + for tiled_object in layer.tiled_objects: + fix_object(tiled_object) + + +def fix_map(map): + map.version = None + map.tiled_version = None + for layer in [layer for layer in map.layers if hasattr(layer, "tiled_objects")]: + fix_layer(layer) + + for tileset in map.tilesets.values(): + fix_tileset(tileset) + + +@pytest.mark.parametrize("parser_type", ["json", "tmx"]) @pytest.mark.parametrize("map_test", ALL_MAP_TESTS) -def test_map_integration(map_test): +def test_map_integration(parser_type, map_test): # it's a PITA to import like this, don't do it # https://stackoverflow.com/a/67692/1342874 spec = importlib.util.spec_from_file_location("expected", map_test / "expected.py") expected = importlib.util.module_from_spec(spec) spec.loader.exec_module(expected) - raw_maps_path = map_test / "map.json" + if parser_type == "json": + raw_maps_path = map_test / "map.json" + elif parser_type == "tmx": + raw_maps_path = map_test / "map.tmx" casted_map = parse_map(raw_maps_path) + # file detection when running from unit tests is broken expected.EXPECTED.map_file = casted_map.map_file + + # who even knows what/how/when the gods determine what the + # version values in maps/tileset files are, so we're just not + # gonna check them, because they don't make sense anyways. + # + # Yes the values could be set to None in the expected objects + # directly, but alas, this is just test code that's already stupid fast + # and I'm lazy because there's too many of them already existing. + fix_map(expected.EXPECTED) + fix_map(casted_map) assert casted_map == expected.EXPECTED From 0efffe9defaca31b83762d97cd1a74d923485523 Mon Sep 17 00:00:00 2001 From: Darren Eberly Date: Tue, 21 Dec 2021 22:16:10 -0500 Subject: [PATCH 13/13] Update changelog and version for 2.0.0 --- CHANGELOG.md | 17 ++++++++++++++++- pytiled_parser/version.py | 2 +- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 73470283..8cb8dbaa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,22 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), -## [Unreleased] +## [2.0.0] - 2021-12-21 + +Welcome to pytiled-parser 2.0! A lot has changed under the hood with this release that has enabled a slew of new features and abilities. Most of the changes here are under the hood, and there is only really one major API change to be aware of. However the under the hood changes and the new features they've enabled are significant enough to call this a major release. + +The entire pytiled-parser API has been abstracted to a common interface, and the parsing functionality completely +seperated from it. This means that we are able to implement parsers for different formats, and enable cross-loading between formats. + +With the release of 2.0, we have added full support for the TMX spec. Meaning you can once again load TMX maps, TSX tilesets, and TX templates with pytiled-parser, just like the pre 1.0 days, except now we have 100% coverage of the spec, and it's behind the same 1.0 API interface you've come to know and love. + +If you're already using pytiled-parser, chances are you don't need to do anything other than upgrade to enable TMX support. The `parse_map` function still works exactly the same, but will now auto-analyze the file given to it and determine what format it is, and choose the parser accordingly. The same will happen for any tilesets that get loaded during this. Meaning you can load JSON tilesets in a TMX map, and TSX tilesets in a JSON map, with no extra configuration. + +The only thing that can't currently be cross-loaded between formats is object templates, if you're using a JSON object template, it will need to be within a JSON map, same for TMX. A `NotImplementedError` will be raised with an appropriate message if you try to cross-load these. Support is planned for this in likely 2.1.0. + +The only API change to be worried about here is related to World file loading. Previously in pytiled-parser if you loaded a World file, it would also parse all maps associated with the world. This is not great behavior, as the intention of worlds is to be able to load and unload maps on the fly. With the previous setup, if you had a large world, then every single map would be loaded into memory at startup and is generally not the behavior you'd want if using world files. + +To remedy this, the `WorldMap.map_file` attribute has been added to store a `pathlib.Path` to the map file. The previous API had a `WorldMap.tiled_map` attribute which was the fully parsed `pytiled_parser.TiledMap` map object. ## [1.5.4] - 2021-10-12 diff --git a/pytiled_parser/version.py b/pytiled_parser/version.py index b06d5b07..d948627b 100644 --- a/pytiled_parser/version.py +++ b/pytiled_parser/version.py @@ -1,3 +1,3 @@ """pytiled_parser version""" -__version__ = "2.0.0-beta" +__version__ = "2.0.0"