diff --git a/pytiled_parser/__init__.py b/pytiled_parser/__init__.py index 46255ba..f37e2ae 100644 --- a/pytiled_parser/__init__.py +++ b/pytiled_parser/__init__.py @@ -12,8 +12,10 @@ PyTiled Parser is not tied to any particular graphics library or game engine. # pylint: disable=too-few-public-methods from .common_types import OrderedPair, Size +from .exception import UnknownFormat from .layer import ImageLayer, Layer, LayerGroup, ObjectLayer, TileLayer +from .parser import parse_map from .properties import Properties -from .tiled_map import TiledMap, parse_map +from .tiled_map import TiledMap from .tileset import Tile, Tileset from .version import __version__ diff --git a/pytiled_parser/exception.py b/pytiled_parser/exception.py new file mode 100644 index 0000000..8d75354 --- /dev/null +++ b/pytiled_parser/exception.py @@ -0,0 +1,2 @@ +class UnknownFormat(Exception): + pass diff --git a/pytiled_parser/layer.py b/pytiled_parser/layer.py index a0237b2..a2cf80b 100644 --- a/pytiled_parser/layer.py +++ b/pytiled_parser/layer.py @@ -8,27 +8,14 @@ See: # pylint: disable=too-few-public-methods -import base64 -import gzip -import importlib.util -import zlib from pathlib import Path -from typing import Any, List, Optional, Union -from typing import cast as type_cast +from typing import List, Optional, Union import attr -from typing_extensions import TypedDict -from . import properties as properties_ -from . import tiled_object -from .common_types import Color, OrderedPair, Size -from .util import parse_color - -zstd_spec = importlib.util.find_spec("zstd") -if zstd_spec: - import zstd # pylint: disable=import-outside-toplevel -else: - zstd = None # pylint: disable=invalid-name +from pytiled_parser.common_types import Color, OrderedPair, Size +from pytiled_parser.properties import Properties +from pytiled_parser.tiled_object import TiledObject @attr.s(auto_attribs=True, kw_only=True) @@ -51,8 +38,8 @@ class Layer: """ name: str - opacity: float - visible: bool + opacity: float = 1 + visible: bool = True coordinates: OrderedPair = OrderedPair(0, 0) parallax_factor: OrderedPair = OrderedPair(1, 1) @@ -60,7 +47,7 @@ class Layer: id: Optional[int] = None size: Optional[Size] = None - properties: Optional[properties_.Properties] = None + properties: Optional[Properties] = None tint_color: Optional[Color] = None @@ -127,7 +114,7 @@ class ObjectLayer(Layer): for more info. """ - tiled_objects: List[tiled_object.TiledObject] + tiled_objects: List[TiledObject] draw_order: Optional[str] = "topdown" @@ -162,341 +149,3 @@ class LayerGroup(Layer): """ layers: Optional[List[Layer]] - - -class RawChunk(TypedDict): - """The keys and their types that appear in a Chunk JSON Object. - - See: https://doc.mapeditor.org/en/stable/reference/json-map-format/#chunk - """ - - data: Union[List[int], str] - height: int - width: int - x: int - y: int - - -class RawLayer(TypedDict): - """The keys and their types that appear in a Layer JSON Object. - - See: https://doc.mapeditor.org/en/stable/reference/json-map-format/#layer - """ - - chunks: List[RawChunk] - compression: str - data: Union[List[int], str] - draworder: str - encoding: str - height: int - id: int - image: str - layers: List[Any] - name: str - objects: List[tiled_object.RawTiledObject] - offsetx: float - offsety: float - parallaxx: float - parallaxy: float - opacity: float - properties: List[properties_.RawProperty] - startx: int - starty: int - tintcolor: str - transparentcolor: str - type: str - visible: bool - width: int - x: int - y: int - - -def _convert_raw_tile_layer_data(data: List[int], layer_width: int) -> List[List[int]]: - """Convert raw layer data into a nested lit based on the layer width - - Args: - data: The data to convert - layer_width: Width of the layer - - Returns: - List[List[int]]: A nested list containing the converted data - """ - tile_grid: List[List[int]] = [[]] - - column_count = 0 - row_count = 0 - for item in data: - column_count += 1 - tile_grid[row_count].append(item) - if not column_count % layer_width and column_count < len(data): - row_count += 1 - tile_grid.append([]) - - return tile_grid - - -def _decode_tile_layer_data( - data: str, compression: str, layer_width: int -) -> List[List[int]]: - """Decode Base64 Encoded tile data. Optionally supports gzip and zlib compression. - - Args: - data: The base64 encoded data - compression: Either zlib, gzip, or empty. If empty no decompression is done. - - Returns: - List[List[int]]: A nested list containing the decoded data - - Raises: - ValueError: For an unsupported compression type. - """ - unencoded_data = base64.b64decode(data) - if compression == "zlib": - unzipped_data = zlib.decompress(unencoded_data) - elif compression == "gzip": - unzipped_data = gzip.decompress(unencoded_data) - elif compression == "zstd" and zstd is None: - raise ValueError( - "zstd compression support is not installed." - "To install use 'pip install pytiled-parser[zstd]'" - ) - elif compression == "zstd": - unzipped_data = zstd.decompress(unencoded_data) - else: - unzipped_data = unencoded_data - - tile_grid: List[int] = [] - - byte_count = 0 - int_count = 0 - int_value = 0 - for byte in unzipped_data: - int_value += byte << (byte_count * 8) - byte_count += 1 - if not byte_count % 4: - byte_count = 0 - int_count += 1 - tile_grid.append(int_value) - int_value = 0 - - return _convert_raw_tile_layer_data(tile_grid, layer_width) - - -def _cast_chunk( - raw_chunk: RawChunk, - encoding: Optional[str] = None, - compression: Optional[str] = None, -) -> Chunk: - """Cast the raw_chunk to a Chunk. - - Args: - raw_chunk: RawChunk to be casted to a Chunk - encoding: Encoding type. ("base64" or None) - compression: Either zlib, gzip, or empty. If empty no decompression is done. - - Returns: - Chunk: The Chunk created from the raw_chunk - """ - if encoding == "base64": - assert isinstance(compression, str) - assert isinstance(raw_chunk["data"], str) - data = _decode_tile_layer_data( - raw_chunk["data"], compression, raw_chunk["width"] - ) - else: - data = _convert_raw_tile_layer_data( - raw_chunk["data"], raw_chunk["width"] # type: ignore - ) - - chunk = Chunk( - coordinates=OrderedPair(raw_chunk["x"], raw_chunk["y"]), - size=Size(raw_chunk["width"], raw_chunk["height"]), - data=data, - ) - - return chunk - - -def _get_common_attributes(raw_layer: RawLayer) -> Layer: - """Create a Layer containing all the attributes common to all layers. - - This is to create the stub Layer object that can then be used to create the actual - specific sub-classes of Layer. - - Args: - raw_layer: Raw Tiled object get common attributes from - - Returns: - Layer: The attributes in common of all layers - """ - common_attributes = Layer( - name=raw_layer["name"], - opacity=raw_layer["opacity"], - visible=raw_layer["visible"], - ) - - # if startx is present, starty is present - if raw_layer.get("startx") is not None: - common_attributes.coordinates = OrderedPair( - raw_layer["startx"], raw_layer["starty"] - ) - - if raw_layer.get("id") is not None: - common_attributes.id = raw_layer["id"] - - # if either width or height is present, they both are - if raw_layer.get("width") is not None: - common_attributes.size = Size(raw_layer["width"], raw_layer["height"]) - - if raw_layer.get("offsetx") is not None: - common_attributes.offset = OrderedPair( - raw_layer["offsetx"], raw_layer["offsety"] - ) - - if raw_layer.get("properties") is not None: - common_attributes.properties = properties_.cast(raw_layer["properties"]) - - parallax = [1.0, 1.0] - - if raw_layer.get("parallaxx") is not None: - parallax[0] = raw_layer["parallaxx"] - - if raw_layer.get("parallaxy") is not None: - parallax[1] = raw_layer["parallaxy"] - - common_attributes.parallax_factor = OrderedPair(parallax[0], parallax[1]) - - if raw_layer.get("tintcolor") is not None: - common_attributes.tint_color = parse_color(raw_layer["tintcolor"]) - - return common_attributes - - -def _cast_tile_layer(raw_layer: RawLayer) -> TileLayer: - """Cast the raw_layer to a TileLayer. - - Args: - raw_layer: RawLayer to be casted to a TileLayer - - Returns: - TileLayer: The TileLayer created from raw_layer - """ - tile_layer = TileLayer(**_get_common_attributes(raw_layer).__dict__) - - if raw_layer.get("chunks") is not None: - tile_layer.chunks = [] - for chunk in raw_layer["chunks"]: - if raw_layer.get("encoding") is not None: - tile_layer.chunks.append( - _cast_chunk(chunk, raw_layer["encoding"], raw_layer["compression"]) - ) - else: - tile_layer.chunks.append(_cast_chunk(chunk)) - - if raw_layer.get("data") is not None: - if raw_layer.get("encoding") is not None: - tile_layer.data = _decode_tile_layer_data( - data=type_cast(str, raw_layer["data"]), - compression=raw_layer["compression"], - layer_width=raw_layer["width"], - ) - else: - tile_layer.data = _convert_raw_tile_layer_data( - raw_layer["data"], raw_layer["width"] # type: ignore - ) - - return tile_layer - - -def _cast_object_layer( - raw_layer: RawLayer, - parent_dir: Optional[Path] = None, -) -> ObjectLayer: - """Cast the raw_layer to an ObjectLayer. - - Args: - raw_layer: RawLayer to be casted to an ObjectLayer - Returns: - ObjectLayer: The ObjectLayer created from raw_layer - """ - - tiled_objects = [] - for tiled_object_ in raw_layer["objects"]: - tiled_objects.append(tiled_object.cast(tiled_object_, parent_dir)) - - return ObjectLayer( - tiled_objects=tiled_objects, - draw_order=raw_layer["draworder"], - **_get_common_attributes(raw_layer).__dict__, - ) - - -def _cast_image_layer(raw_layer: RawLayer) -> ImageLayer: - """Cast the raw_layer to a ImageLayer. - - Args: - raw_layer: RawLayer to be casted to a ImageLayer - - Returns: - ImageLayer: The ImageLayer created from raw_layer - """ - image_layer = ImageLayer( - image=Path(raw_layer["image"]), **_get_common_attributes(raw_layer).__dict__ - ) - - if raw_layer.get("transparentcolor") is not None: - image_layer.transparent_color = parse_color(raw_layer["transparentcolor"]) - - return image_layer - - -def _cast_group_layer( - raw_layer: RawLayer, parent_dir: Optional[Path] = None -) -> LayerGroup: - """Cast the raw_layer to a LayerGroup. - - Args: - raw_layer: RawLayer to be casted to a LayerGroup - - Returns: - LayerGroup: The LayerGroup created from raw_layer - """ - - layers = [] - - for layer in raw_layer["layers"]: - layers.append(cast(layer, parent_dir=parent_dir)) - - return LayerGroup(layers=layers, **_get_common_attributes(raw_layer).__dict__) - - -def cast( - raw_layer: RawLayer, - parent_dir: Optional[Path] = None, -) -> Layer: - """Cast a raw Tiled layer into a pytiled_parser type. - - This function will determine the type of layer and cast accordingly. - - Args: - raw_layer: Raw layer to be cast. - parent_dir: The parent directory that the map file is in. - - Returns: - Layer: a properly typed Layer. - - Raises: - RuntimeError: For an invalid layer type being provided - """ - type_ = raw_layer["type"] - - if type_ == "objectgroup": - return _cast_object_layer(raw_layer, parent_dir) - elif type_ == "group": - return _cast_group_layer(raw_layer, parent_dir) - elif type_ == "imagelayer": - return _cast_image_layer(raw_layer) - elif type_ == "tilelayer": - return _cast_tile_layer(raw_layer) - - raise RuntimeError(f"An invalid layer type of {type_} was supplied") diff --git a/pytiled_parser/parser.py b/pytiled_parser/parser.py new file mode 100644 index 0000000..9a0a389 --- /dev/null +++ b/pytiled_parser/parser.py @@ -0,0 +1,29 @@ +from pathlib import Path + +from pytiled_parser import UnknownFormat +from pytiled_parser.parsers.json.tiled_map import parse as json_map_parse +from pytiled_parser.parsers.tmx.tiled_map import parse as tmx_map_parse +from pytiled_parser.tiled_map import TiledMap +from pytiled_parser.util import check_format + + +def parse_map(file: Path) -> TiledMap: + """Parse the raw Tiled map into a pytiled_parser type + + Args: + file: Path to the map file + + Returns: + Tiledmap: a properly typed TiledMap + """ + parser = check_format(file) + + # The type ignores are because mypy for some reaosn thinks those functions return Any + if parser == "tmx": + return tmx_map_parse(file) # type: ignore + elif parser == "json": + return json_map_parse(file) # type: ignore + else: + raise UnknownFormat( + "Unknown Map Format, please use either the TMX or JSON format." + ) diff --git a/pytiled_parser/parsers/json/layer.py b/pytiled_parser/parsers/json/layer.py new file mode 100644 index 0000000..4953045 --- /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 0000000..4e9896f --- /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 0000000..82f6c04 --- /dev/null +++ b/pytiled_parser/parsers/json/tiled_map.py @@ -0,0 +1,170 @@ +import json +import xml.etree.ElementTree as etree +from pathlib import Path +from typing import List, Union, cast + +from typing_extensions import TypedDict + +from pytiled_parser.common_types import Size +from pytiled_parser.exception import UnknownFormat +from pytiled_parser.parsers.json.layer import RawLayer +from pytiled_parser.parsers.json.layer import parse as parse_layer +from pytiled_parser.parsers.json.properties import RawProperty +from pytiled_parser.parsers.json.properties import parse as parse_properties +from pytiled_parser.parsers.json.tileset import RawTileSet +from pytiled_parser.parsers.json.tileset import parse as parse_json_tileset +from pytiled_parser.parsers.tmx.tileset import parse as parse_tmx_tileset +from pytiled_parser.tiled_map import TiledMap, TilesetDict +from pytiled_parser.util import check_format, parse_color + + +class RawTilesetMapping(TypedDict): + + firstgid: int + source: str + + +class RawTiledMap(TypedDict): + """The keys and their types that appear in a Tiled JSON Map Object. + + Tiled Docs: https://doc.mapeditor.org/en/stable/reference/json-map-format/#map + """ + + backgroundcolor: str + compressionlevel: int + height: int + hexsidelength: int + infinite: bool + layers: List[RawLayer] + nextlayerid: int + nextobjectid: int + orientation: str + properties: List[RawProperty] + renderorder: str + staggeraxis: str + staggerindex: str + tiledversion: str + tileheight: int + tilesets: List[RawTilesetMapping] + tilewidth: int + type: str + version: Union[str, float] + width: int + + +def parse(file: Path) -> TiledMap: + """Parse the raw Tiled map into a pytiled_parser type. + + Args: + file: Path to the map file. + + Returns: + TiledMap: A parsed TiledMap. + """ + with open(file) as map_file: + raw_tiled_map = json.load(map_file) + + parent_dir = file.parent + + raw_tilesets: List[Union[RawTileSet, RawTilesetMapping]] = raw_tiled_map["tilesets"] + tilesets: TilesetDict = {} + + for raw_tileset in raw_tilesets: + if raw_tileset.get("source") is not None: + # Is an external Tileset + tileset_path = Path(parent_dir / raw_tileset["source"]) + parser = check_format(tileset_path) + with open(tileset_path) as raw_tileset_file: + if parser == "json": + tilesets[raw_tileset["firstgid"]] = parse_json_tileset( + json.load(raw_tileset_file), + raw_tileset["firstgid"], + external_path=tileset_path.parent, + ) + elif parser == "tmx": + raw_tileset_external = etree.parse(raw_tileset_file).getroot() + tilesets[raw_tileset["firstgid"]] = parse_tmx_tileset( + raw_tileset_external, + raw_tileset["firstgid"], + external_path=tileset_path.parent, + ) + else: + raise UnknownFormat( + "Unkown Tileset format, please use either the TSX or JSON format." + ) + + else: + # Is an embedded Tileset + raw_tileset = cast(RawTileSet, raw_tileset) + tilesets[raw_tileset["firstgid"]] = parse_json_tileset( + raw_tileset, raw_tileset["firstgid"] + ) + + if isinstance(raw_tiled_map["version"], float): + version = str(raw_tiled_map["version"]) + else: + version = raw_tiled_map["version"] + + # `map` is a built-in function + map_ = TiledMap( + map_file=file, + infinite=raw_tiled_map["infinite"], + layers=[parse_layer(layer_, parent_dir) for layer_ in raw_tiled_map["layers"]], + map_size=Size(raw_tiled_map["width"], raw_tiled_map["height"]), + next_layer_id=raw_tiled_map["nextlayerid"], + next_object_id=raw_tiled_map["nextobjectid"], + orientation=raw_tiled_map["orientation"], + render_order=raw_tiled_map["renderorder"], + tiled_version=raw_tiled_map["tiledversion"], + tile_size=Size(raw_tiled_map["tilewidth"], raw_tiled_map["tileheight"]), + tilesets=tilesets, + version=version, + ) + + layers = [layer for layer in map_.layers if hasattr(layer, "tiled_objects")] + + for my_layer in layers: + for tiled_object in my_layer.tiled_objects: # type: ignore + if hasattr(tiled_object, "new_tileset"): + if tiled_object.new_tileset: + already_loaded = None + for val in map_.tilesets.values(): + if val.name == tiled_object.new_tileset["name"]: + already_loaded = val + break + + if not already_loaded: + highest_firstgid = max(map_.tilesets.keys()) + last_tileset_count = map_.tilesets[highest_firstgid].tile_count + new_firstgid = highest_firstgid + last_tileset_count + map_.tilesets[new_firstgid] = parse_json_tileset( + tiled_object.new_tileset, + new_firstgid, + tiled_object.new_tileset_path, + ) + tiled_object.gid = tiled_object.gid + (new_firstgid - 1) + + else: + tiled_object.gid = tiled_object.gid + ( + already_loaded.firstgid - 1 + ) + + tiled_object.new_tileset = None + tiled_object.new_tileset_path = None + + if raw_tiled_map.get("backgroundcolor") is not None: + map_.background_color = parse_color(raw_tiled_map["backgroundcolor"]) + + if raw_tiled_map.get("hexsidelength") is not None: + map_.hex_side_length = raw_tiled_map["hexsidelength"] + + if raw_tiled_map.get("properties") is not None: + map_.properties = parse_properties(raw_tiled_map["properties"]) + + if raw_tiled_map.get("staggeraxis") is not None: + map_.stagger_axis = raw_tiled_map["staggeraxis"] + + if raw_tiled_map.get("staggerindex") is not None: + map_.stagger_index = raw_tiled_map["staggerindex"] + + return map_ diff --git a/pytiled_parser/parsers/json/tiled_object.py b/pytiled_parser/parsers/json/tiled_object.py new file mode 100644 index 0000000..2acc4b3 --- /dev/null +++ b/pytiled_parser/parsers/json/tiled_object.py @@ -0,0 +1,321 @@ +"""Object parsing for the JSON Map Format. +""" +import json +import xml.etree.ElementTree as etree +from pathlib import Path +from typing import Any, Callable, Dict, List, Optional + +from typing_extensions import TypedDict + +from pytiled_parser.common_types import OrderedPair, Size +from pytiled_parser.parsers.json.properties import RawProperty +from pytiled_parser.parsers.json.properties import parse as parse_properties +from pytiled_parser.tiled_object import ( + Ellipse, + Point, + Polygon, + Polyline, + Rectangle, + Text, + Tile, + TiledObject, +) +from pytiled_parser.util import load_object_template, parse_color + + +class RawText(TypedDict): + """The keys and their types that appear in a Tiled JSON Text Object. + + Tiled Doc: https://doc.mapeditor.org/en/stable/reference/json-map-format/#text-example + """ + + text: str + color: str + + fontfamily: str + pixelsize: float # this is `font_size` in Text + + bold: bool + italic: bool + strikeout: bool + underline: bool + kerning: bool + + halign: str + valign: str + wrap: bool + + +class RawObject(TypedDict): + """The keys and their types that appear in a Tiled JSON Object. + + Tiled Doc: https://doc.mapeditor.org/en/stable/reference/json-map-format/#object + """ + + id: int + gid: int + template: str + x: float + y: float + width: float + height: float + rotation: float + visible: bool + name: str + type: str + properties: List[RawProperty] + ellipse: bool + point: bool + polygon: List[Dict[str, float]] + polyline: List[Dict[str, float]] + text: RawText + + +def _parse_common(raw_object: RawObject) -> TiledObject: + """Create an Object containing all the attributes common to all types of objects. + + Args: + raw_object: Raw object to get common attributes from + + Returns: + Object: The attributes in common of all types of objects + """ + + common = TiledObject( + id=raw_object["id"], + coordinates=OrderedPair(raw_object["x"], raw_object["y"]), + visible=raw_object["visible"], + size=Size(raw_object["width"], raw_object["height"]), + rotation=raw_object["rotation"], + name=raw_object["name"], + type=raw_object["type"], + ) + + if raw_object.get("properties") is not None: + common.properties = parse_properties(raw_object["properties"]) + + return common + + +def _parse_ellipse(raw_object: RawObject) -> Ellipse: + """Parse the raw object into an Ellipse. + + Args: + raw_object: Raw object to be parsed to an Ellipse + + Returns: + Ellipse: The Ellipse object created from the raw object + """ + return Ellipse(**_parse_common(raw_object).__dict__) + + +def _parse_rectangle(raw_object: RawObject) -> Rectangle: + """Parse the raw object into a Rectangle. + + Args: + raw_object: Raw object to be parsed to a Rectangle + + Returns: + Rectangle: The Rectangle object created from the raw object + """ + return Rectangle(**_parse_common(raw_object).__dict__) + + +def _parse_point(raw_object: RawObject) -> Point: + """Parse the raw object into a Point. + + Args: + raw_object: Raw object to be parsed to a Point + + Returns: + Point: The Point object created from the raw object + """ + return Point(**_parse_common(raw_object).__dict__) + + +def _parse_polygon(raw_object: RawObject) -> Polygon: + """Parse the raw object into a Polygon. + + Args: + raw_object: Raw object to be parsed to a Polygon + + Returns: + Polygon: The Polygon object created from the raw object + """ + polygon = [] + for point in raw_object["polygon"]: + polygon.append(OrderedPair(point["x"], point["y"])) + + return Polygon(points=polygon, **_parse_common(raw_object).__dict__) + + +def _parse_polyline(raw_object: RawObject) -> Polyline: + """Parse the raw object into a Polyline. + + Args: + raw_object: Raw object to be parsed to a Polyline + + Returns: + Polyline: The Polyline object created from the raw object + """ + polyline = [] + for point in raw_object["polyline"]: + polyline.append(OrderedPair(point["x"], point["y"])) + + return Polyline(points=polyline, **_parse_common(raw_object).__dict__) + + +def _parse_tile( + raw_object: RawObject, + new_tileset: Optional[Dict[str, Any]] = None, + new_tileset_path: Optional[Path] = None, +) -> Tile: + """Parse the raw object into a Tile. + + Args: + raw_object: Raw object to be parsed to a Tile + + Returns: + Tile: The Tile object created from the raw object + """ + gid = raw_object["gid"] + + return Tile( + gid=gid, + new_tileset=new_tileset, + new_tileset_path=new_tileset_path, + **_parse_common(raw_object).__dict__ + ) + + +def _parse_text(raw_object: RawObject) -> Text: + """Parse the raw object into Text. + + Args: + raw_object: Raw object to be parsed to a Text + + Returns: + Text: The Text object created from the raw object + """ + # required attributes + raw_text: RawText = raw_object["text"] + text = raw_text["text"] + + # create base Text object + text_object = Text(text=text, **_parse_common(raw_object).__dict__) + + # optional attributes + if raw_text.get("color") is not None: + text_object.color = parse_color(raw_text["color"]) + + if raw_text.get("fontfamily") is not None: + text_object.font_family = raw_text["fontfamily"] + + if raw_text.get("pixelsize") is not None: + text_object.font_size = raw_text["pixelsize"] + + if raw_text.get("bold") is not None: + text_object.bold = raw_text["bold"] + + if raw_text.get("italic") is not None: + text_object.italic = raw_text["italic"] + + if raw_text.get("kerning") is not None: + text_object.kerning = raw_text["kerning"] + + if raw_text.get("strikeout") is not None: + text_object.strike_out = raw_text["strikeout"] + + if raw_text.get("underline") is not None: + text_object.underline = raw_text["underline"] + + if raw_text.get("halign") is not None: + text_object.horizontal_align = raw_text["halign"] + + if raw_text.get("valign") is not None: + text_object.vertical_align = raw_text["valign"] + + if raw_text.get("wrap") is not None: + text_object.wrap = raw_text["wrap"] + + return text_object + + +def _get_parser(raw_object: RawObject) -> Callable[[RawObject], TiledObject]: + """Get the parser function for a given raw object. + + Only used internally by the JSON parser. + + Args: + raw_object: Raw object that is analyzed to determine the parser function. + + Returns: + Callable[[RawObject], Object]: The parser function. + """ + if raw_object.get("ellipse"): + return _parse_ellipse + + if raw_object.get("point"): + return _parse_point + + if raw_object.get("gid"): + # Only tile objects have the `gid` key + return _parse_tile + + if raw_object.get("polygon"): + return _parse_polygon + + if raw_object.get("polyline"): + return _parse_polyline + + if raw_object.get("text"): + return _parse_text + + # If it's none of the above, rectangle is the only one left. + # Rectangle is the only object which has no special properties to signify that. + return _parse_rectangle + + +def parse( + raw_object: RawObject, + parent_dir: Optional[Path] = None, +) -> TiledObject: + """Parse the raw object into a pytiled_parser version + + Args: + raw_object: Raw object that is to be cast. + parent_dir: The parent directory that the map file is in. + + Returns: + Object: A parsed Object. + + Raises: + RuntimeError: When a parameter that is conditionally required was not sent. + """ + new_tileset = None + new_tileset_path = None + + if raw_object.get("template"): + if not parent_dir: + raise RuntimeError( + "A parent directory must be specified when using object templates." + ) + template_path = Path(parent_dir / raw_object["template"]) + template, new_tileset, new_tileset_path = load_object_template(template_path) + + if isinstance(template, dict): + loaded_template = template["object"] + for key in loaded_template: + if key != "id": + raw_object[key] = loaded_template[key] # type: ignore + elif isinstance(template, etree.Element): + # load the XML object into the JSON object + raise NotImplementedError( + "Loading TMX object templates inside a JSON map is currently not supported, " + "but will be in a future release." + ) + + if raw_object.get("gid"): + return _parse_tile(raw_object, new_tileset, new_tileset_path) + + return _get_parser(raw_object)(raw_object) diff --git a/pytiled_parser/parsers/json/tileset.py b/pytiled_parser/parsers/json/tileset.py new file mode 100644 index 0000000..3206bac --- /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 0000000..ea68905 --- /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/parsers/tmx/layer.py b/pytiled_parser/parsers/tmx/layer.py new file mode 100644 index 0000000..4ba1fa1 --- /dev/null +++ b/pytiled_parser/parsers/tmx/layer.py @@ -0,0 +1,360 @@ +"""Layer parsing for the TMX Map Format. +""" +import base64 +import gzip +import importlib.util +import xml.etree.ElementTree as etree +import zlib +from pathlib import Path +from typing import List, Optional + +from pytiled_parser.common_types import OrderedPair, Size +from pytiled_parser.layer import ( + Chunk, + ImageLayer, + Layer, + LayerGroup, + ObjectLayer, + TileLayer, +) +from pytiled_parser.parsers.tmx.properties import parse as parse_properties +from pytiled_parser.parsers.tmx.tiled_object import parse as parse_object +from pytiled_parser.util import parse_color + +zstd_spec = importlib.util.find_spec("zstd") +if zstd_spec: + import zstd +else: + zstd = None + + +def _convert_raw_tile_layer_data(data: List[int], layer_width: int) -> List[List[int]]: + """Convert raw layer data into a nested lit based on the layer width + + Args: + data: The data to convert + layer_width: Width of the layer + + Returns: + List[List[int]]: A nested list containing the converted data + """ + tile_grid: List[List[int]] = [[]] + + column_count = 0 + row_count = 0 + for item in data: + column_count += 1 + tile_grid[row_count].append(item) + if not column_count % layer_width and column_count < len(data): + row_count += 1 + tile_grid.append([]) + + return tile_grid + + +def _decode_tile_layer_data( + data: str, compression: str, layer_width: int +) -> List[List[int]]: + """Decode Base64 Encoded tile data. Optionally supports gzip and zlib compression. + + Args: + data: The base64 encoded data + compression: Either zlib, gzip, or empty. If empty no decompression is done. + + Returns: + List[List[int]]: A nested list containing the decoded data + + Raises: + ValueError: For an unsupported compression type. + """ + unencoded_data = base64.b64decode(data) + if compression == "zlib": + unzipped_data = zlib.decompress(unencoded_data) + elif compression == "gzip": + unzipped_data = gzip.decompress(unencoded_data) + elif compression == "zstd" and zstd is None: + raise ValueError( + "zstd compression support is not installed." + "To install use 'pip install pytiled-parser[zstd]'" + ) + elif compression == "zstd": + unzipped_data = zstd.decompress(unencoded_data) + else: + unzipped_data = unencoded_data + + tile_grid: List[int] = [] + + byte_count = 0 + int_count = 0 + int_value = 0 + for byte in unzipped_data: + int_value += byte << (byte_count * 8) + byte_count += 1 + if not byte_count % 4: + byte_count = 0 + int_count += 1 + tile_grid.append(int_value) + int_value = 0 + + return _convert_raw_tile_layer_data(tile_grid, layer_width) + + +def _parse_chunk( + raw_chunk: etree.Element, + encoding: Optional[str] = None, + compression: Optional[str] = None, +) -> Chunk: + """Parse the raw_chunk to a Chunk. + + Args: + raw_chunk: XML Element to be parsed to a Chunk + encoding: Encoding type. ("base64" or None) + compression: Either zlib, gzip, or empty. If empty no decompression is done. + + Returns: + Chunk: The Chunk created from the raw_chunk + """ + if encoding == "base64": + assert isinstance(compression, str) + data = _decode_tile_layer_data( + raw_chunk.text, compression, int(raw_chunk.attrib["width"]) # type: ignore + ) + else: + data = _convert_raw_tile_layer_data( + [int(v.strip()) for v in raw_chunk.text.split(",")], # type: ignore + int(raw_chunk.attrib["width"]), + ) + + return Chunk( + coordinates=OrderedPair(int(raw_chunk.attrib["x"]), int(raw_chunk.attrib["y"])), + size=Size(int(raw_chunk.attrib["width"]), int(raw_chunk.attrib["height"])), + data=data, + ) + + +def _parse_common(raw_layer: etree.Element) -> Layer: + """Create a Layer containing all the attributes common to all layer types. + + This is to create the stub Layer object that can then be used to create the actual + specific sub-classes of Layer. + + Args: + raw_layer: XML Element to get common attributes from + + Returns: + Layer: The attributes in common of all layer types + """ + if raw_layer.attrib.get("name") is None: + raw_layer.attrib["name"] = "" + + common = Layer( + name=raw_layer.attrib["name"], + ) + + if raw_layer.attrib.get("opacity") is not None: + common.opacity = float(raw_layer.attrib["opacity"]) + + if raw_layer.attrib.get("visible") is not None: + common.visible = bool(int(raw_layer.attrib["visible"])) + + if raw_layer.attrib.get("id") is not None: + common.id = int(raw_layer.attrib["id"]) + + if raw_layer.attrib.get("offsetx") is not None: + common.offset = OrderedPair( + float(raw_layer.attrib["offsetx"]), float(raw_layer.attrib["offsety"]) + ) + + properties_element = raw_layer.find("./properties") + if properties_element is not None: + common.properties = parse_properties(properties_element) + + parallax = [1.0, 1.0] + + if raw_layer.attrib.get("parallaxx") is not None: + parallax[0] = float(raw_layer.attrib["parallaxx"]) + + if raw_layer.attrib.get("parallaxy") is not None: + parallax[1] = float(raw_layer.attrib["parallaxy"]) + + common.parallax_factor = OrderedPair(parallax[0], parallax[1]) + + if raw_layer.attrib.get("tintcolor") is not None: + common.tint_color = parse_color(raw_layer.attrib["tintcolor"]) + + return common + + +def _parse_tile_layer(raw_layer: etree.Element) -> TileLayer: + """Parse the raw_layer to a TileLayer. + + Args: + raw_layer: XML Element to be parsed to a TileLayer. + + Returns: + TileLayer: The TileLayer created from raw_layer + """ + common = _parse_common(raw_layer).__dict__ + del common["size"] + tile_layer = TileLayer( + size=Size(int(raw_layer.attrib["width"]), int(raw_layer.attrib["height"])), + **common, + ) + + data_element = raw_layer.find("data") + if data_element is not None: + encoding = None + if data_element.attrib.get("encoding") is not None: + encoding = data_element.attrib["encoding"] + + compression = "" + if data_element.attrib.get("compression") is not None: + compression = data_element.attrib["compression"] + + raw_chunks = data_element.findall("chunk") + if not raw_chunks: + if encoding and encoding != "csv": + tile_layer.data = _decode_tile_layer_data( + data=data_element.text, # type: ignore + compression=compression, + layer_width=int(raw_layer.attrib["width"]), + ) + else: + tile_layer.data = _convert_raw_tile_layer_data( + [int(v.strip()) for v in data_element.text.split(",")], # type: ignore + int(raw_layer.attrib["width"]), + ) + else: + chunks = [] + for raw_chunk in raw_chunks: + chunks.append( + _parse_chunk( + raw_chunk, + encoding, + compression, + ) + ) + + if chunks: + tile_layer.chunks = chunks + + return tile_layer + + +def _parse_object_layer( + raw_layer: etree.Element, parent_dir: Optional[Path] = None +) -> ObjectLayer: + """Parse the raw_layer to an ObjectLayer. + + Args: + raw_layer: XML Element to be parsed to an ObjectLayer. + + Returns: + ObjectLayer: The ObjectLayer created from raw_layer + """ + objects = [] + for object_ in raw_layer.findall("./object"): + objects.append(parse_object(object_, parent_dir)) + + object_layer = ObjectLayer( + tiled_objects=objects, + **_parse_common(raw_layer).__dict__, + ) + + if raw_layer.attrib.get("draworder") is not None: + object_layer.draw_order = raw_layer.attrib["draworder"] + + return object_layer + + +def _parse_image_layer(raw_layer: etree.Element) -> ImageLayer: + """Parse the raw_layer to an ImageLayer. + + Args: + raw_layer: XML Element to be parsed to an ImageLayer. + + Returns: + ImageLayer: The ImageLayer created from raw_layer + """ + image_element = raw_layer.find("./image") + if image_element is not None: + source = Path(image_element.attrib["source"]) + + transparent_color = None + if image_element.attrib.get("trans") is not None: + transparent_color = parse_color(image_element.attrib["trans"]) + + image_layer = ImageLayer( + image=source, + transparent_color=transparent_color, + **_parse_common(raw_layer).__dict__, + ) + print(image_layer.size) + return image_layer + + raise RuntimeError("Tried to parse an image layer that doesn't have an image!") + + +def _parse_group_layer( + raw_layer: etree.Element, parent_dir: Optional[Path] = None +) -> LayerGroup: + """Parse the raw_layer to a LayerGroup. + + Args: + raw_layer: XML Element to be parsed to a LayerGroup. + + Returns: + LayerGroup: The LayerGroup created from raw_layer + """ + layers: List[Layer] = [] + for layer in raw_layer.findall("./layer"): + layers.append(_parse_tile_layer(layer)) + + for layer in raw_layer.findall("./objectgroup"): + layers.append(_parse_object_layer(layer, parent_dir)) + + for layer in raw_layer.findall("./imagelayer"): + layers.append(_parse_image_layer(layer)) + + for layer in raw_layer.findall("./group"): + layers.append(_parse_group_layer(layer, parent_dir)) + # layers = [] + # layers = [ + # parse(child_layer, parent_dir=parent_dir) + # for child_layer in raw_layer.iter() + # if child_layer.tag in ["layer", "objectgroup", "imagelayer", "group"] + # ] + + return LayerGroup(layers=layers, **_parse_common(raw_layer).__dict__) + + +def parse( + raw_layer: etree.Element, + parent_dir: Optional[Path] = None, +) -> Layer: + """Parse a raw Layer into a pytiled_parser object. + + This function will determine the type of layer and parse accordingly. + + Args: + raw_layer: Raw layer to be parsed. + parent_dir: The parent directory that the map file is in. + + Returns: + Layer: A parsed Layer. + + Raises: + RuntimeError: For an invalid layer type being provided + """ + type_ = raw_layer.tag + + if type_ == "objectgroup": + return _parse_object_layer(raw_layer, parent_dir) + elif type_ == "group": + return _parse_group_layer(raw_layer, parent_dir) + elif type_ == "imagelayer": + return _parse_image_layer(raw_layer) + elif type_ == "layer": + return _parse_tile_layer(raw_layer) + + raise RuntimeError(f"An invalid layer type of {type_} was supplied") diff --git a/pytiled_parser/parsers/tmx/properties.py b/pytiled_parser/parsers/tmx/properties.py new file mode 100644 index 0000000..173463b --- /dev/null +++ b/pytiled_parser/parsers/tmx/properties.py @@ -0,0 +1,33 @@ +import xml.etree.ElementTree as etree +from pathlib import Path +from typing import List, Union, cast + +from pytiled_parser.properties import Properties, Property +from pytiled_parser.util import parse_color + + +def parse(raw_properties: etree.Element) -> Properties: + + final: Properties = {} + value: Property + + for raw_property in raw_properties.findall("property"): + + type_ = raw_property.attrib.get("type") + value_ = raw_property.attrib["value"] + if type_ == "file": + value = Path(value_) + elif type_ == "color": + value = parse_color(value_) + elif type_ == "int" or type_ == "float": + value = float(value_) + elif type_ == "bool": + if value_ == "true": + value = True + else: + value = False + else: + value = value_ + final[raw_property.attrib["name"]] = value + + return final diff --git a/pytiled_parser/parsers/tmx/tiled_map.py b/pytiled_parser/parsers/tmx/tiled_map.py new file mode 100644 index 0000000..f12c6e2 --- /dev/null +++ b/pytiled_parser/parsers/tmx/tiled_map.py @@ -0,0 +1,132 @@ +import json +import xml.etree.ElementTree as etree +from pathlib import Path + +from pytiled_parser.common_types import OrderedPair, Size +from pytiled_parser.exception import UnknownFormat +from pytiled_parser.parsers.json.tileset import parse as parse_json_tileset +from pytiled_parser.parsers.tmx.layer import parse as parse_layer +from pytiled_parser.parsers.tmx.properties import parse as parse_properties +from pytiled_parser.parsers.tmx.tileset import parse as parse_tmx_tileset +from pytiled_parser.tiled_map import TiledMap, TilesetDict +from pytiled_parser.util import check_format, parse_color + + +def parse(file: Path) -> TiledMap: + """Parse the raw Tiled map into a pytiled_parser type. + + Args: + file: Path to the map file. + + Returns: + TiledMap: A parsed TiledMap. + """ + with open(file) as map_file: + raw_map = etree.parse(map_file).getroot() + + parent_dir = file.parent + + raw_tilesets = raw_map.findall("./tileset") + tilesets: TilesetDict = {} + + for raw_tileset in raw_tilesets: + if raw_tileset.attrib.get("source") is not None: + # Is an external Tileset + tileset_path = Path(parent_dir / raw_tileset.attrib["source"]) + parser = check_format(tileset_path) + with open(tileset_path) as tileset_file: + if parser == "tmx": + raw_tileset_external = etree.parse(tileset_file).getroot() + tilesets[int(raw_tileset.attrib["firstgid"])] = parse_tmx_tileset( + raw_tileset_external, + int(raw_tileset.attrib["firstgid"]), + external_path=tileset_path.parent, + ) + elif parser == "json": + tilesets[int(raw_tileset.attrib["firstgid"])] = parse_json_tileset( + json.load(tileset_file), + int(raw_tileset.attrib["firstgid"]), + external_path=tileset_path.parent, + ) + else: + raise UnknownFormat( + "Unkown Tileset format, please use either the TSX or JSON format." + ) + + else: + # Is an embedded Tileset + tilesets[int(raw_tileset.attrib["firstgid"])] = parse_tmx_tileset( + raw_tileset, int(raw_tileset.attrib["firstgid"]) + ) + + layers = [] + for element in raw_map.iter(): + if element.tag in ["layer", "objectgroup", "imagelayer", "group"]: + layers.append(parse_layer(element, parent_dir)) + + map_ = TiledMap( + map_file=file, + infinite=bool(int(raw_map.attrib["infinite"])), + layers=layers, + map_size=Size(int(raw_map.attrib["width"]), int(raw_map.attrib["height"])), + next_layer_id=int(raw_map.attrib["nextlayerid"]), + next_object_id=int(raw_map.attrib["nextobjectid"]), + orientation=raw_map.attrib["orientation"], + render_order=raw_map.attrib["renderorder"], + tiled_version=raw_map.attrib["tiledversion"], + tile_size=Size( + int(raw_map.attrib["tilewidth"]), int(raw_map.attrib["tileheight"]) + ), + tilesets=tilesets, + version=raw_map.attrib["version"], + ) + + layers = [layer for layer in map_.layers if hasattr(layer, "tiled_objects")] + + for my_layer in layers: + for tiled_object in my_layer.tiled_objects: + if hasattr(tiled_object, "new_tileset"): + if tiled_object.new_tileset is not None: + already_loaded = None + for val in map_.tilesets.values(): + if val.name == tiled_object.new_tileset.attrib["name"]: + already_loaded = val + break + + if not already_loaded: + print("here") + highest_firstgid = max(map_.tilesets.keys()) + last_tileset_count = map_.tilesets[highest_firstgid].tile_count + new_firstgid = highest_firstgid + last_tileset_count + map_.tilesets[new_firstgid] = parse_tmx_tileset( + tiled_object.new_tileset, + new_firstgid, + tiled_object.new_tileset_path, + ) + tiled_object.gid = tiled_object.gid + (new_firstgid - 1) + + else: + tiled_object.gid = tiled_object.gid + ( + already_loaded.firstgid - 1 + ) + + tiled_object.new_tileset = None + tiled_object.new_tileset_path = None + + if raw_map.attrib.get("backgroundcolor") is not None: + map_.background_color = parse_color(raw_map.attrib["backgroundcolor"]) + + if raw_map.attrib.get("hexsidelength") is not None: + map_.hex_side_length = int(raw_map.attrib["hexsidelength"]) + + properties_element = raw_map.find("./properties") + if properties_element: + map_.properties = parse_properties(properties_element) + + if raw_map.attrib.get("staggeraxis") is not None: + map_.stagger_axis = raw_map.attrib["staggeraxis"] + + if raw_map.attrib.get("staggerindex") is not None: + map_.stagger_index = raw_map.attrib["staggerindex"] + + return map_ diff --git a/pytiled_parser/parsers/tmx/tiled_object.py b/pytiled_parser/parsers/tmx/tiled_object.py new file mode 100644 index 0000000..ceb4e08 --- /dev/null +++ b/pytiled_parser/parsers/tmx/tiled_object.py @@ -0,0 +1,293 @@ +import json +import xml.etree.ElementTree as etree +from pathlib import Path +from typing import Callable, Optional + +from pytiled_parser.common_types import OrderedPair, Size +from pytiled_parser.parsers.tmx.properties import parse as parse_properties +from pytiled_parser.tiled_object import ( + Ellipse, + Point, + Polygon, + Polyline, + Rectangle, + Text, + Tile, + TiledObject, +) +from pytiled_parser.util import load_object_template, parse_color + + +def _parse_common(raw_object: etree.Element) -> TiledObject: + """Create an Object containing all the attributes common to all types of objects. + + Args: + raw_object: XML Element to get common attributes from + + Returns: + Object: The attributes in common of all types of objects + """ + + common = TiledObject( + id=int(raw_object.attrib["id"]), + coordinates=OrderedPair( + float(raw_object.attrib["x"]), float(raw_object.attrib["y"]) + ), + ) + + if raw_object.attrib.get("width") is not None: + common.size = Size( + float(raw_object.attrib["width"]), float(raw_object.attrib["height"]) + ) + + if raw_object.attrib.get("visible") is not None: + common.visible = bool(int(raw_object.attrib["visible"])) + + if raw_object.attrib.get("rotation") is not None: + common.rotation = float(raw_object.attrib["rotation"]) + + if raw_object.attrib.get("name") is not None: + common.name = raw_object.attrib["name"] + + if raw_object.attrib.get("type") is not None: + common.type = raw_object.attrib["type"] + + properties_element = raw_object.find("./properties") + if properties_element: + common.properties = parse_properties(properties_element) + + return common + + +def _parse_ellipse(raw_object: etree.Element) -> Ellipse: + """Parse the raw object into an Ellipse. + + Args: + raw_object: XML Element to be parsed to an Ellipse + + Returns: + Ellipse: The Ellipse object created from the raw object + """ + return Ellipse(**_parse_common(raw_object).__dict__) + + +def _parse_rectangle(raw_object: etree.Element) -> Rectangle: + """Parse the raw object into a Rectangle. + + Args: + raw_object: XML Element to be parsed to a Rectangle + + Returns: + Rectangle: The Rectangle object created from the raw object + """ + return Rectangle(**_parse_common(raw_object).__dict__) + + +def _parse_point(raw_object: etree.Element) -> Point: + """Parse the raw object into a Point. + + Args: + raw_object: XML Element to be parsed to a Point + + Returns: + Point: The Point object created from the raw object + """ + return Point(**_parse_common(raw_object).__dict__) + + +def _parse_polygon(raw_object: etree.Element) -> Polygon: + """Parse the raw object into a Polygon. + + Args: + raw_object: XML Element to be parsed to a Polygon + + Returns: + Polygon: The Polygon object created from the raw object + """ + polygon = [] + polygon_element = raw_object.find("./polygon") + if polygon_element is not None: + for raw_point in polygon_element.attrib["points"].split(" "): + point = raw_point.split(",") + polygon.append(OrderedPair(float(point[0]), float(point[1]))) + + return Polygon(points=polygon, **_parse_common(raw_object).__dict__) + + +def _parse_polyline(raw_object: etree.Element) -> Polyline: + """Parse the raw object into a Polyline. + + Args: + raw_object: Raw object to be parsed to a Polyline + + Returns: + Polyline: The Polyline object created from the raw object + """ + polyline = [] + polyline_element = raw_object.find("./polyline") + if polyline_element is not None: + for raw_point in polyline_element.attrib["points"].split(" "): + point = raw_point.split(",") + polyline.append(OrderedPair(float(point[0]), float(point[1]))) + + return Polyline(points=polyline, **_parse_common(raw_object).__dict__) + + +def _parse_tile( + raw_object: etree.Element, + new_tileset: Optional[etree.Element] = None, + new_tileset_path: Optional[Path] = None, +) -> Tile: + """Parse the raw object into a Tile. + + Args: + raw_object: XML Element to be parsed to a Tile + + Returns: + Tile: The Tile object created from the raw object + """ + return Tile( + gid=int(raw_object.attrib["gid"]), + new_tileset=new_tileset, + new_tileset_path=new_tileset_path, + **_parse_common(raw_object).__dict__ + ) + + +def _parse_text(raw_object: etree.Element) -> Text: + """Parse the raw object into Text. + + Args: + raw_object: XML Element to be parsed to a Text + + Returns: + Text: The Text object created from the raw object + """ + # required attributes + text_element = raw_object.find("./text") + + if text_element is not None: + text = text_element.text + + if not text: + text = "" + # create base Text object + text_object = Text(text=text, **_parse_common(raw_object).__dict__) + + # optional attributes + + if text_element.attrib.get("color") is not None: + text_object.color = parse_color(text_element.attrib["color"]) + + if text_element.attrib.get("fontfamily") is not None: + text_object.font_family = text_element.attrib["fontfamily"] + + if text_element.attrib.get("pixelsize") is not None: + text_object.font_size = float(text_element.attrib["pixelsize"]) + + if text_element.attrib.get("bold") is not None: + text_object.bold = bool(int(text_element.attrib["bold"])) + + if text_element.attrib.get("italic") is not None: + text_object.italic = bool(int(text_element.attrib["italic"])) + + if text_element.attrib.get("kerning") is not None: + text_object.kerning = bool(int(text_element.attrib["kerning"])) + + if text_element.attrib.get("strikeout") is not None: + text_object.strike_out = bool(int(text_element.attrib["strikeout"])) + + if text_element.attrib.get("underline") is not None: + text_object.underline = bool(int(text_element.attrib["underline"])) + + if text_element.attrib.get("halign") is not None: + text_object.horizontal_align = text_element.attrib["halign"] + + if text_element.attrib.get("valign") is not None: + text_object.vertical_align = text_element.attrib["valign"] + + if text_element.attrib.get("wrap") is not None: + text_object.wrap = bool(int(text_element.attrib["wrap"])) + + return text_object + + +def _get_parser(raw_object: etree.Element) -> Callable[[etree.Element], TiledObject]: + """Get the parser function for a given raw object. + + Only used internally by the TMX parser. + + Args: + raw_object: XML Element that is analyzed to determine the parser function. + + Returns: + Callable[[Element], Object]: The parser function. + """ + if raw_object.find("./ellipse") is not None: + return _parse_ellipse + + if raw_object.find("./point") is not None: + return _parse_point + + if raw_object.find("./polygon") is not None: + return _parse_polygon + + if raw_object.find("./polyline") is not None: + return _parse_polyline + + if raw_object.find("./text") is not None: + return _parse_text + + # If it's none of the above, rectangle is the only one left. + # Rectangle is the only object which has no properties to signify that. + return _parse_rectangle + + +def parse(raw_object: etree.Element, parent_dir: Optional[Path] = None) -> TiledObject: + """Parse the raw object into a pytiled_parser version + + Args: + raw_object: XML Element that is to be parsed. + parent_dir: The parent directory that the map file is in. + + Returns: + TiledObject: A parsed Object. + + Raises: + RuntimeError: When a parameter that is conditionally required was not sent. + """ + new_tileset = None + new_tileset_path = None + + if raw_object.attrib.get("template"): + if not parent_dir: + raise RuntimeError( + "A parent directory must be specified when using object templates." + ) + template_path = Path(parent_dir / raw_object.attrib["template"]) + template, new_tileset, new_tileset_path = load_object_template(template_path) + + if isinstance(template, etree.Element): + new_object = template.find("./object") + if new_object is not None: + if raw_object.attrib.get("id") is not None: + new_object.attrib["id"] = raw_object.attrib["id"] + + if raw_object.attrib.get("x") is not None: + new_object.attrib["x"] = raw_object.attrib["x"] + + if raw_object.attrib.get("y") is not None: + new_object.attrib["y"] = raw_object.attrib["y"] + + raw_object = new_object + elif isinstance(template, dict): + # load the JSON object into the XML object + raise NotImplementedError( + "Loading JSON object templates inside a TMX map is currently not supported, " + "but will be in a future release." + ) + + if raw_object.attrib.get("gid"): + return _parse_tile(raw_object, new_tileset, new_tileset_path) + + return _get_parser(raw_object)(raw_object) diff --git a/pytiled_parser/parsers/tmx/tileset.py b/pytiled_parser/parsers/tmx/tileset.py new file mode 100644 index 0000000..712d1cf --- /dev/null +++ b/pytiled_parser/parsers/tmx/tileset.py @@ -0,0 +1,194 @@ +import xml.etree.ElementTree as etree +from pathlib import Path +from typing import Optional + +from pytiled_parser.common_types import OrderedPair +from pytiled_parser.parsers.tmx.layer import parse as parse_layer +from pytiled_parser.parsers.tmx.properties import parse as parse_properties +from pytiled_parser.parsers.tmx.wang_set import parse as parse_wangset +from pytiled_parser.tileset import Frame, Grid, Tile, Tileset, Transformations +from pytiled_parser.util import parse_color + + +def _parse_frame(raw_frame: etree.Element) -> Frame: + """Parse the raw_frame to a Frame object. + + Args: + raw_frame: XML Element to be parsed to a Frame + + Returns: + Frame: The Frame created from the raw_frame + """ + + return Frame( + duration=int(raw_frame.attrib["duration"]), + tile_id=int(raw_frame.attrib["tileid"]), + ) + + +def _parse_grid(raw_grid: etree.Element) -> Grid: + """Parse the raw_grid to a Grid object. + + Args: + raw_grid: XML Element to be parsed to a Grid + + Returns: + Grid: The Grid created from the raw_grid + """ + + return Grid( + orientation=raw_grid.attrib["orientation"], + width=int(raw_grid.attrib["width"]), + height=int(raw_grid.attrib["height"]), + ) + + +def _parse_transformations(raw_transformations: etree.Element) -> Transformations: + """Parse the raw_transformations to a Transformations object. + + Args: + raw_transformations: XML Element to be parsed to a Transformations + + Returns: + Transformations: The Transformations created from the raw_transformations + """ + + return Transformations( + hflip=bool(int(raw_transformations.attrib["hflip"])), + vflip=bool(int(raw_transformations.attrib["vflip"])), + rotate=bool(int(raw_transformations.attrib["rotate"])), + prefer_untransformed=bool( + int(raw_transformations.attrib["preferuntransformed"]) + ), + ) + + +def _parse_tile(raw_tile: etree.Element, external_path: Optional[Path] = None) -> Tile: + """Parse the raw_tile to a Tile object. + + Args: + raw_tile: XML Element to be parsed to a Tile + + Returns: + Tile: The Tile created from the raw_tile + """ + + tile = Tile(id=int(raw_tile.attrib["id"])) + + if raw_tile.attrib.get("type") is not None: + tile.type = raw_tile.attrib["type"] + + animation_element = raw_tile.find("./animation") + if animation_element is not None: + tile.animation = [] + for raw_frame in animation_element.findall("./frame"): + tile.animation.append(_parse_frame(raw_frame)) + + object_element = raw_tile.find("./objectgroup") + if object_element is not None: + tile.objects = parse_layer(object_element) + + properties_element = raw_tile.find("./properties") + if properties_element is not None: + tile.properties = parse_properties(properties_element) + + image_element = raw_tile.find("./image") + if image_element is not None: + if external_path: + tile.image = ( + Path(external_path / image_element.attrib["source"]) + .absolute() + .resolve() + ) + else: + tile.image = Path(image_element.attrib["source"]) + + tile.image_width = int(image_element.attrib["width"]) + tile.image_height = int(image_element.attrib["height"]) + + return tile + + +def parse( + raw_tileset: etree.Element, + firstgid: int, + external_path: Optional[Path] = None, +) -> Tileset: + tileset = Tileset( + name=raw_tileset.attrib["name"], + tile_count=int(raw_tileset.attrib["tilecount"]), + tile_width=int(raw_tileset.attrib["tilewidth"]), + tile_height=int(raw_tileset.attrib["tileheight"]), + columns=int(raw_tileset.attrib["columns"]), + firstgid=firstgid, + ) + + if raw_tileset.attrib.get("version") is not None: + tileset.version = raw_tileset.attrib["version"] + + if raw_tileset.attrib.get("tiledversion") is not None: + tileset.tiled_version = raw_tileset.attrib["tiledversion"] + + if raw_tileset.attrib.get("backgroundcolor") is not None: + tileset.background_color = parse_color(raw_tileset.attrib["backgroundcolor"]) + + if raw_tileset.attrib.get("spacing") is not None: + tileset.spacing = int(raw_tileset.attrib["spacing"]) + + if raw_tileset.attrib.get("margin") is not None: + tileset.margin = int(raw_tileset.attrib["margin"]) + + image_element = raw_tileset.find("image") + if image_element is not None: + if external_path: + tileset.image = ( + Path(external_path / image_element.attrib["source"]) + .absolute() + .resolve() + ) + else: + tileset.image = Path(image_element.attrib["source"]) + + tileset.image_width = int(image_element.attrib["width"]) + tileset.image_height = int(image_element.attrib["height"]) + + if image_element.attrib.get("trans") is not None: + my_string = image_element.attrib["trans"] + if my_string[0] != "#": + my_string = f"#{my_string}" + tileset.transparent_color = parse_color(my_string) + + tileoffset_element = raw_tileset.find("./tileoffset") + if tileoffset_element is not None: + tileset.tile_offset = OrderedPair( + int(tileoffset_element.attrib["x"]), int(tileoffset_element.attrib["y"]) + ) + + grid_element = raw_tileset.find("./grid") + if grid_element is not None: + tileset.grid = _parse_grid(grid_element) + + properties_element = raw_tileset.find("./properties") + if properties_element is not None: + tileset.properties = parse_properties(properties_element) + + tiles = {} + for tile_element in raw_tileset.findall("./tile"): + tiles[int(tile_element.attrib["id"])] = _parse_tile( + tile_element, external_path=external_path + ) + if tiles: + tileset.tiles = tiles + + wangsets_element = raw_tileset.find("./wangsets") + if wangsets_element is not None: + wangsets = [] + for raw_wangset in wangsets_element.findall("./wangset"): + wangsets.append(parse_wangset(raw_wangset)) + tileset.wang_sets = wangsets + + transformations_element = raw_tileset.find("./transformations") + if transformations_element is not None: + tileset.transformations = _parse_transformations(transformations_element) + + return tileset diff --git a/pytiled_parser/parsers/tmx/wang_set.py b/pytiled_parser/parsers/tmx/wang_set.py new file mode 100644 index 0000000..b167226 --- /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 diff --git a/pytiled_parser/properties.py b/pytiled_parser/properties.py index e9b87b5..f8bc0ac 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 1f56885..9c1c1ba 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 3d167d0..a96d65a 100644 --- a/pytiled_parser/tiled_object.py +++ b/pytiled_parser/tiled_object.py @@ -1,14 +1,12 @@ # pylint: disable=too-few-public-methods -import json +import xml.etree.ElementTree as etree from pathlib import Path -from typing import Any, Callable, Dict, List, Optional, Union +from typing import Any, Dict, List, Optional, Union import attr -from typing_extensions import TypedDict from . import properties as properties_ from .common_types import Color, OrderedPair, Size -from .util import parse_color @attr.s(auto_attribs=True, kw_only=True) @@ -37,10 +35,9 @@ class TiledObject: coordinates: OrderedPair size: Size = Size(0, 0) rotation: float = 0 - visible: bool - - name: Optional[str] = None - type: Optional[str] = None + visible: bool = True + name: str = "" + type: str = "" properties: properties_.Properties = {} @@ -148,302 +145,5 @@ class Tile(TiledObject): """ gid: int - new_tileset: Optional[Dict[str, Any]] = None + new_tileset: Optional[Union[etree.Element, Dict[str, Any]]] = None new_tileset_path: Optional[Path] = None - - -class RawTextDict(TypedDict): - """ The keys and their types that appear in a Text JSON Object.""" - - text: str - color: str - - fontfamily: str - pixelsize: float # this is `font_size` in Text - - bold: bool - italic: bool - strikeout: bool - underline: bool - kerning: bool - - halign: str - valign: str - wrap: bool - - -class RawTiledObject(TypedDict): - """ The keys and their types that appear in a Tiled JSON Object.""" - - id: int - gid: int - template: str - x: float - y: float - width: float - height: float - rotation: float - visible: bool - name: str - type: str - properties: List[properties_.RawProperty] - ellipse: bool - point: bool - polygon: List[Dict[str, float]] - polyline: List[Dict[str, float]] - text: Dict[str, Union[float, str]] - - -RawTiledObjects = List[RawTiledObject] - - -def _get_common_attributes(raw_tiled_object: RawTiledObject) -> TiledObject: - """Create a TiledObject containing all the attributes common to all tiled objects - - Args: - raw_tiled_object: Raw Tiled object get common attributes from - - Returns: - TiledObject: The attributes in common of all Tiled Objects - """ - - common_attributes = TiledObject( - id=raw_tiled_object["id"], - coordinates=OrderedPair(raw_tiled_object["x"], raw_tiled_object["y"]), - visible=raw_tiled_object["visible"], - size=Size(raw_tiled_object["width"], raw_tiled_object["height"]), - rotation=raw_tiled_object["rotation"], - name=raw_tiled_object["name"], - type=raw_tiled_object["type"], - ) - - if raw_tiled_object.get("properties") is not None: - common_attributes.properties = properties_.cast(raw_tiled_object["properties"]) - - return common_attributes - - -def _cast_ellipse(raw_tiled_object: RawTiledObject) -> Ellipse: - """Cast the raw_tiled_object to an Ellipse object. - - Args: - raw_tiled_object: Raw Tiled object to be casted to an Ellipse - - Returns: - Ellipse: The Ellipse object created from the raw_tiled_object - """ - return Ellipse(**_get_common_attributes(raw_tiled_object).__dict__) - - -def _cast_rectangle(raw_tiled_object: RawTiledObject) -> Rectangle: - """Cast the raw_tiled_object to a Rectangle object. - - Args: - raw_tiled_object: Raw Tiled object to be casted to a Rectangle - - Returns: - Rectangle: The Rectangle object created from the raw_tiled_object - """ - return Rectangle(**_get_common_attributes(raw_tiled_object).__dict__) - - -def _cast_point(raw_tiled_object: RawTiledObject) -> Point: - """Cast the raw_tiled_object to a Point object. - - Args: - raw_tiled_object: Raw Tiled object to be casted to a Point - - Returns: - Point: The Point object created from the raw_tiled_object - """ - return Point(**_get_common_attributes(raw_tiled_object).__dict__) - - -def _cast_tile( - raw_tiled_object: RawTiledObject, - new_tileset: Optional[Dict[str, Any]] = None, - new_tileset_path: Optional[Path] = None, -) -> Tile: - """Cast the raw_tiled_object to a Tile object. - - Args: - raw_tiled_object: Raw Tiled object to be casted to a Tile - - Returns: - Tile: The Tile object created from the raw_tiled_object - """ - gid = raw_tiled_object["gid"] - - return Tile( - gid=gid, - new_tileset=new_tileset, - new_tileset_path=new_tileset_path, - **_get_common_attributes(raw_tiled_object).__dict__ - ) - - -def _cast_polygon(raw_tiled_object: RawTiledObject) -> Polygon: - """Cast the raw_tiled_object to a Polygon object. - - Args: - raw_tiled_object: Raw Tiled object to be casted to a Polygon - - Returns: - Polygon: The Polygon object created from the raw_tiled_object - """ - polygon = [] - for point in raw_tiled_object["polygon"]: - polygon.append(OrderedPair(point["x"], point["y"])) - - return Polygon(points=polygon, **_get_common_attributes(raw_tiled_object).__dict__) - - -def _cast_polyline(raw_tiled_object: RawTiledObject) -> Polyline: - """Cast the raw_tiled_object to a Polyline object. - - Args: - raw_tiled_object: Raw Tiled Object to be casted to a Polyline - - Returns: - Polyline: The Polyline object created from the raw_tiled_object - """ - polyline = [] - for point in raw_tiled_object["polyline"]: - polyline.append(OrderedPair(point["x"], point["y"])) - - return Polyline( - points=polyline, **_get_common_attributes(raw_tiled_object).__dict__ - ) - - -def _cast_text(raw_tiled_object: RawTiledObject) -> Text: - """Cast the raw_tiled_object to a Text object. - - Args: - raw_tiled_object: Raw Tiled object to be casted to a Text object - - Returns: - Text: The Text object created from the raw_tiled_object - """ - # required attributes - raw_text_dict: RawTextDict = raw_tiled_object["text"] - text = raw_text_dict["text"] - - # create base Text object - text_object = Text(text=text, **_get_common_attributes(raw_tiled_object).__dict__) - - # optional attributes - if raw_text_dict.get("color") is not None: - text_object.color = parse_color(raw_text_dict["color"]) - - if raw_text_dict.get("fontfamily") is not None: - text_object.font_family = raw_text_dict["fontfamily"] - - if raw_text_dict.get("pixelsize") is not None: - text_object.font_size = raw_text_dict["pixelsize"] - - if raw_text_dict.get("bold") is not None: - text_object.bold = raw_text_dict["bold"] - - if raw_text_dict.get("italic") is not None: - text_object.italic = raw_text_dict["italic"] - - if raw_text_dict.get("kerning") is not None: - text_object.kerning = raw_text_dict["kerning"] - - if raw_text_dict.get("strikeout") is not None: - text_object.strike_out = raw_text_dict["strikeout"] - - if raw_text_dict.get("underline") is not None: - text_object.underline = raw_text_dict["underline"] - - if raw_text_dict.get("halign") is not None: - text_object.horizontal_align = raw_text_dict["halign"] - - if raw_text_dict.get("valign") is not None: - text_object.vertical_align = raw_text_dict["valign"] - - if raw_text_dict.get("wrap") is not None: - text_object.wrap = raw_text_dict["wrap"] - - return text_object - - -def _get_caster( - raw_tiled_object: RawTiledObject, -) -> Callable[[RawTiledObject], TiledObject]: - """Get the caster function for the raw tiled object. - - Args: - raw_tiled_object: Raw Tiled object that is analysed to determine which caster - to return. - - Returns: - Callable[[RawTiledObject], TiledObject]: The caster function. - """ - if raw_tiled_object.get("ellipse"): - return _cast_ellipse - - if raw_tiled_object.get("point"): - return _cast_point - - if raw_tiled_object.get("gid"): - # Only Tile objects have the `gid` key (I think) - return _cast_tile - - if raw_tiled_object.get("polygon"): - return _cast_polygon - - if raw_tiled_object.get("polyline"): - return _cast_polyline - - if raw_tiled_object.get("text"): - return _cast_text - - return _cast_rectangle - - -def cast( - raw_tiled_object: RawTiledObject, - parent_dir: Optional[Path] = None, -) -> TiledObject: - """Cast the raw tiled object into a pytiled_parser type - - Args: - raw_tiled_object: Raw Tiled object that is to be cast. - parent_dir: The parent directory that the map file is in. - - Returns: - TiledObject: a properly typed Tiled object. - - Raises: - RuntimeError: When a required parameter was not sent based on a condition. - """ - new_tileset = None - new_tileset_path = None - - if raw_tiled_object.get("template"): - if not parent_dir: - raise RuntimeError( - "A parent directory must be specified when using object templates" - ) - template_path = Path(parent_dir / raw_tiled_object["template"]) - with open(template_path) as raw_template_file: - template = json.load(raw_template_file) - if "tileset" in template: - tileset_path = Path( - template_path.parent / template["tileset"]["source"] - ) - with open(tileset_path) as raw_tileset_file: - new_tileset = json.load(raw_tileset_file) - new_tileset_path = tileset_path.parent - - loaded_template = template["object"] - for key in loaded_template: - if key != "id": - raw_tiled_object[key] = loaded_template[key] # type: ignore - - if raw_tiled_object.get("gid"): - return _cast_tile(raw_tiled_object, new_tileset, new_tileset_path) - - return _get_caster(raw_tiled_object)(raw_tiled_object) diff --git a/pytiled_parser/tileset.py b/pytiled_parser/tileset.py index 25bd190..de48013 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/util.py b/pytiled_parser/util.py index 75c7d1d..f8bb18b 100644 --- a/pytiled_parser/util.py +++ b/pytiled_parser/util.py @@ -1,4 +1,8 @@ """Utility Functions for PyTiled""" +import json +import xml.etree.ElementTree as etree +from pathlib import Path +from typing import Any from pytiled_parser.common_types import Color @@ -27,3 +31,52 @@ def parse_color(color: str) -> Color: ) raise ValueError("Improperly formatted color passed to parse_color") + + +def check_format(file_path: Path) -> str: + with open(file_path) as file: + line = file.readline().rstrip().strip() + if line[0] == "<": + return "tmx" + else: + return "json" + + +def load_object_template(file_path: Path) -> Any: + template_format = check_format(file_path) + + new_tileset = None + new_tileset_path = None + + if template_format == "tmx": + with open(file_path) as template_file: + template = etree.parse(template_file).getroot() + + tileset_element = template.find("./tileset") + if tileset_element is not None: + tileset_path = Path(file_path.parent / tileset_element.attrib["source"]) + new_tileset = load_object_tileset(tileset_path) + new_tileset_path = tileset_path.parent + elif template_format == "json": + with open(file_path) as template_file: + template = json.load(template_file) + if "tileset" in template: + tileset_path = Path(file_path.parent / template["tileset"]["source"]) # type: ignore + new_tileset = load_object_tileset(tileset_path) + new_tileset_path = tileset_path.parent + + return (template, new_tileset, new_tileset_path) + + +def load_object_tileset(file_path: Path) -> Any: + tileset_format = check_format(file_path) + + new_tileset = None + + with open(file_path) as tileset_file: + if tileset_format == "tmx": + new_tileset = etree.parse(tileset_file).getroot() + elif tileset_format == "json": + new_tileset = json.load(tileset_file) + + return new_tileset diff --git a/pytiled_parser/version.py b/pytiled_parser/version.py index b7bdf02..b06d5b0 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 011410f..9241742 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 5d35322..797ff39 100644 --- a/pytiled_parser/world.py +++ b/pytiled_parser/world.py @@ -8,8 +8,9 @@ from typing import List import attr from typing_extensions import TypedDict -from .common_types import OrderedPair, Size -from .tiled_map import TiledMap, parse_map +from pytiled_parser.common_types import OrderedPair, Size +from pytiled_parser.parser import parse_map +from pytiled_parser.tiled_map import TiledMap @attr.s(auto_attribs=True) @@ -55,7 +56,7 @@ class RawWorld(TypedDict): onlyShowAdjacentMaps: bool -def _cast_world_map(raw_world_map: RawWorldMap, map_file: Path) -> WorldMap: +def _parse_world_map(raw_world_map: RawWorldMap, map_file: Path) -> WorldMap: """Parse the RawWorldMap into a WorldMap. Args: @@ -94,7 +95,7 @@ def parse_world(file: Path) -> World: if raw_world.get("maps"): for raw_map in raw_world["maps"]: map_path = Path(parent_dir / raw_map["fileName"]) - maps.append(_cast_world_map(raw_map, map_path)) + maps.append(_parse_world_map(raw_map, map_path)) if raw_world.get("patterns"): for raw_pattern in raw_world["patterns"]: @@ -131,7 +132,7 @@ def parse_world(file: Path) -> World: } map_path = Path(parent_dir / map_file) - maps.append(_cast_world_map(raw_world_map, map_path)) + maps.append(_parse_world_map(raw_world_map, map_path)) world = World(maps=maps) diff --git a/setup.cfg b/setup.cfg index 86ce232..2ccb360 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_cross_template/map.json b/tests/test_cross_template/map.json new file mode 100644 index 0000000..451323f --- /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 0000000..b77d63b --- /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 0000000..fc39229 --- /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 0000000..6daa364 --- /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 0000000..a1f8d86 --- /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 0000000..c0cbf4e --- /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 0000000..9c59779 --- /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 0000000..3302884 --- /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 0000000..8aee17a --- /dev/null +++ b/tests/test_cross_template/tileset.tsx @@ -0,0 +1,4 @@ + + + + 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 0000000..c94d181 --- /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 0000000..8aee17a --- /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 0000000..060ebe1 --- /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 0000000..8aee17a --- /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 0000000..21316ba --- /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 0000000..8aee17a --- /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 0000000..343bd2b --- /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 0000000..8aee17a --- /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 0000000..2a826bb --- /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 0000000..d83d427 --- /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 0000000..fb995a3 --- /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 0000000..8aee17a --- /dev/null +++ b/tests/test_data/layer_tests/no_layers/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 0000000..c39ca67 --- /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 0000000..9c19e15 --- /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 0000000..192b15e --- /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 0000000..482cf3c --- /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 0000000..cba4d04 --- /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 0000000..7e71558 --- /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 0000000..8b1cf24 --- /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 0000000..1ef8b7a --- /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 0000000..8b1cf24 --- /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 0000000..24cc2f0 --- /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 0000000..6daa364 --- /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 0000000..989b725 --- /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 0000000..d958c77 --- /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 0000000..9c59779 --- /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 0000000..c881c11 --- /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 0000000..8aee17a --- /dev/null +++ b/tests/test_data/map_tests/template/tileset.tsx @@ -0,0 +1,4 @@ + + + + diff --git a/tests/test_data/tilesets/image/tileset.tsx b/tests/test_data/tilesets/image/tileset.tsx new file mode 100644 index 0000000..8aee17a --- /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 0000000..25cae9b --- /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 0000000..62ef87e --- /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 0000000..42478ae --- /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 0000000..7c0c1bc --- /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 0000000..5b20f69 --- /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 0000000..5ab0346 --- /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 0000000..877cf79 --- /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 0000000..2a57297 --- /dev/null +++ b/tests/test_data/tilesets/terrain/tileset.tsx @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/test_layer.py b/tests/test_layer.py index 117761b..bce74b3 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 import layer +from pytiled_parser.common_types import OrderedPair, Size +from pytiled_parser.parsers.json.layer import parse as parse_json +from pytiled_parser.parsers.tmx.layer import parse as parse_tmx TESTS_DIR = Path(os.path.dirname(os.path.abspath(__file__))) TEST_DATA = TESTS_DIR / "test_data" @@ -25,8 +28,36 @@ ALL_LAYER_TESTS = [ ] +def fix_object(my_object): + my_object.coordinates = OrderedPair( + round(my_object.coordinates[0], 4), + round(my_object.coordinates[1], 4), + ) + my_object.size = Size(round(my_object.size[0], 4), round(my_object.size[1], 4)) + + +def fix_layer(layer): + layer.offset = OrderedPair(round(layer.offset[0], 3), round(layer.offset[1], 3)) + layer.coordinates = OrderedPair( + round(layer.coordinates[0], 4), round(layer.coordinates[1], 4) + ) + if layer.size: + layer.size = Size(round(layer.size[0], 4), round(layer.size[1], 4)) + layer.parallax_factor = OrderedPair( + round(layer.parallax_factor[0], 4), + round(layer.parallax_factor[1], 4), + ) + if hasattr(layer, "tiled_objects"): + for tiled_object in layer.tiled_objects: + fix_object(tiled_object) + if hasattr(layer, "layers"): + for child_layer in layer.layers: + fix_layer(child_layer) + + +@pytest.mark.parametrize("parser_type", ["json", "tmx"]) @pytest.mark.parametrize("layer_test", ALL_LAYER_TESTS) -def test_layer_integration(layer_test): +def test_layer_integration(parser_type, layer_test): # it's a PITA to import like this, don't do it # https://stackoverflow.com/a/67692/1342874 spec = importlib.util.spec_from_file_location( @@ -35,10 +66,33 @@ def test_layer_integration(layer_test): expected = importlib.util.module_from_spec(spec) spec.loader.exec_module(expected) - raw_layers_path = layer_test / "map.json" + if parser_type == "json": + raw_layers_path = layer_test / "map.json" + with open(raw_layers_path) as raw_layers_file: + raw_layers = json.load(raw_layers_file)["layers"] + layers = [parse_json(raw_layer) for raw_layer in raw_layers] + elif parser_type == "tmx": + raw_layers_path = layer_test / "map.tmx" + with open(raw_layers_path) as raw_layers_file: + raw_layer = etree.parse(raw_layers_file).getroot() + layers = [] + for layer in raw_layer.findall("./layer"): + layers.append(parse_tmx(layer)) - with open(raw_layers_path) as raw_layers_file: - raw_layers = json.load(raw_layers_file)["layers"] - layers = [layer.cast(raw_layer) for raw_layer in raw_layers] + for layer in raw_layer.findall("./objectgroup"): + layers.append(parse_tmx(layer)) + + for layer in raw_layer.findall("./group"): + layers.append(parse_tmx(layer)) + + for layer in raw_layer.findall("./imagelayer"): + layers.append(parse_tmx(layer)) + + for layer in layers: + fix_layer(layer) + + for layer in expected.EXPECTED: + fix_layer(layer) + print(layer.size) assert layers == expected.EXPECTED diff --git a/tests/test_map.py b/tests/test_map.py index 0019238..424e802 100644 --- a/tests/test_map.py +++ b/tests/test_map.py @@ -5,7 +5,8 @@ from pathlib import Path import pytest -from pytiled_parser import tiled_map +from pytiled_parser import parse_map +from pytiled_parser.common_types import OrderedPair, Size TESTS_DIR = Path(os.path.dirname(os.path.abspath(__file__))) TEST_DATA = TESTS_DIR / "test_data" @@ -21,17 +22,64 @@ ALL_MAP_TESTS = [ ] +def fix_object(my_object): + my_object.coordinates = OrderedPair( + round(my_object.coordinates[0], 3), round(my_object.coordinates[1], 3) + ) + my_object.size = Size(round(my_object.size[0], 4), round(my_object.size[1], 4)) + + +def fix_tileset(tileset): + tileset.version = None + tileset.tiled_version = None + if tileset.tiles: + for tile in tileset.tiles.values(): + if tile.objects: + for my_object in tile.objects.tiled_objects: + fix_object(my_object) + + +def fix_layer(layer): + for tiled_object in layer.tiled_objects: + fix_object(tiled_object) + + +def fix_map(map): + map.version = None + map.tiled_version = None + for layer in [layer for layer in map.layers if hasattr(layer, "tiled_objects")]: + fix_layer(layer) + + for tileset in map.tilesets.values(): + fix_tileset(tileset) + + +@pytest.mark.parametrize("parser_type", ["json", "tmx"]) @pytest.mark.parametrize("map_test", ALL_MAP_TESTS) -def test_map_integration(map_test): +def test_map_integration(parser_type, map_test): # it's a PITA to import like this, don't do it # https://stackoverflow.com/a/67692/1342874 spec = importlib.util.spec_from_file_location("expected", map_test / "expected.py") expected = importlib.util.module_from_spec(spec) spec.loader.exec_module(expected) - raw_maps_path = map_test / "map.json" + if parser_type == "json": + raw_maps_path = map_test / "map.json" + elif parser_type == "tmx": + raw_maps_path = map_test / "map.tmx" - casted_map = tiled_map.parse_map(raw_maps_path) + casted_map = parse_map(raw_maps_path) + # file detection when running from unit tests is broken expected.EXPECTED.map_file = casted_map.map_file + + # who even knows what/how/when the gods determine what the + # version values in maps/tileset files are, so we're just not + # gonna check them, because they don't make sense anyways. + # + # Yes the values could be set to None in the expected objects + # directly, but alas, this is just test code that's already stupid fast + # and I'm lazy because there's too many of them already existing. + fix_map(expected.EXPECTED) + fix_map(casted_map) assert casted_map == expected.EXPECTED diff --git a/tests/test_tiled_object.py b/tests/test_tiled_object_json.py similarity index 96% rename from tests/test_tiled_object.py rename to tests/test_tiled_object_json.py index 10e71c4..eacbc3e 100644 --- a/tests/test_tiled_object.py +++ b/tests/test_tiled_object_json.py @@ -5,7 +5,17 @@ from pathlib import Path import pytest -from pytiled_parser import common_types, tiled_object +from pytiled_parser import common_types +from pytiled_parser.parsers.json.tiled_object import parse +from pytiled_parser.tiled_object import ( + Ellipse, + Point, + Polygon, + Polyline, + Rectangle, + Text, + Tile, +) ELLIPSES = [ ( @@ -23,7 +33,7 @@ ELLIPSES = [ "y":81.1913152210981 } """, - tiled_object.Ellipse( + Ellipse( id=6, size=common_types.Size(57.4013868364215, 18.5517790155735), name="name: ellipse", @@ -48,7 +58,7 @@ ELLIPSES = [ "y":53.9092872570194 } """, - tiled_object.Ellipse( + Ellipse( id=7, size=common_types.Size(6.32943048766625, 31.4288962146186), name="name: ellipse - invisible", @@ -73,7 +83,7 @@ ELLIPSES = [ "y":120.040923041946 } """, - tiled_object.Ellipse( + Ellipse( id=8, size=common_types.Size(29.6828464249176, 24.2264408321018), name="name: ellipse - rotated", @@ -98,7 +108,7 @@ ELLIPSES = [ "y":127.679890871888 } """, - tiled_object.Ellipse( + Ellipse( id=29, name="name: ellipse - no width or height", rotation=0, @@ -124,7 +134,7 @@ RECTANGLES = [ "y":23.571672160964 } """, - tiled_object.Rectangle( + Rectangle( id=1, size=common_types.Size(45.3972945322269, 41.4686825053996), name="name: rectangle", @@ -148,7 +158,7 @@ RECTANGLES = [ "y":91.0128452881664 } """, - tiled_object.Rectangle( + Rectangle( id=4, size=common_types.Size(30.9923837671934, 32.7384335568944), name="name: rectangle - invisible", @@ -172,7 +182,7 @@ RECTANGLES = [ "y":23.3534159372513 } """, - tiled_object.Rectangle( + Rectangle( id=5, size=common_types.Size(10, 22), name="name: rectangle - rotated", @@ -196,7 +206,7 @@ RECTANGLES = [ "y":53.4727748095942 } """, - tiled_object.Rectangle( + Rectangle( id=28, size=common_types.Size(0, 0), name="name: rectangle - no width or height", @@ -251,7 +261,7 @@ RECTANGLES = [ "y":131.826759122428 } """, - tiled_object.Rectangle( + Rectangle( id=30, size=common_types.Size(21.170853700125, 13.7501420938956), name="name: rectangle - properties", @@ -287,7 +297,7 @@ POINTS = [ "y":82.9373650107991 } """, - tiled_object.Point( + Point( id=2, name="name: point", rotation=0, @@ -311,7 +321,7 @@ POINTS = [ "y":95.8144822098443 } """, - tiled_object.Point( + Point( id=3, name="name: point invisible", rotation=0, @@ -338,7 +348,7 @@ TILES = [ "y":48.3019211094691 } """, - tiled_object.Tile( + Tile( id=13, size=common_types.Size(32, 32), name="name: tile", @@ -364,7 +374,7 @@ TILES = [ "y":168.779356598841 } """, - tiled_object.Tile( + Tile( id=14, size=common_types.Size(32, 32), name="name: tile - invisible", @@ -390,7 +400,7 @@ TILES = [ "y":59.8695009662385 } """, - tiled_object.Tile( + Tile( id=15, size=common_types.Size(32, 32), name="name: tile - horizontal flipped", @@ -416,7 +426,7 @@ TILES = [ "y":60.742525861089 } """, - tiled_object.Tile( + Tile( id=16, size=common_types.Size(32, 32), name="name: tile - vertical flipped", @@ -442,7 +452,7 @@ TILES = [ "y":95.6635216551097 } """, - tiled_object.Tile( + Tile( id=17, size=common_types.Size(32, 32), name="name: tile - both flipped", @@ -468,7 +478,7 @@ TILES = [ "y":142.62 } """, - tiled_object.Tile( + Tile( id=18, size=common_types.Size(32, 32), name="name: tile - rotated", @@ -517,7 +527,7 @@ POLYGONS = [ "y":38.6313515971354 } """, - tiled_object.Polygon( + Polygon( id=9, name="name: polygon", points=[ @@ -560,7 +570,7 @@ POLYGONS = [ "y":24.4446970558145 } """, - tiled_object.Polygon( + Polygon( id=10, name="name: polygon - invisible", points=[ @@ -613,7 +623,7 @@ POLYGONS = [ "y":19.8613163578493 } """, - tiled_object.Polygon( + Polygon( id=11, name="name: polygon - rotated", points=[ @@ -660,7 +670,7 @@ POLYLINES = [ "y":90.1398203933159 } """, - tiled_object.Polyline( + Polyline( id=12, name="name: polyline", points=[ @@ -701,7 +711,7 @@ POLYLINES = [ "y":163.333333333333 } """, - tiled_object.Polyline( + Polyline( id=31, name="name: polyline - invisible", points=[ @@ -742,7 +752,7 @@ POLYLINES = [ "y":128.666666666667 } """, - tiled_object.Polyline( + Polyline( id=32, name="name: polyline - rotated", points=[ @@ -778,7 +788,7 @@ TEXTS = [ "y":93.2986813686484 } """, - tiled_object.Text( + Text( id=19, name="name: text", text="Hello World", @@ -809,7 +819,7 @@ TEXTS = [ "y":112.068716607935 } """, - tiled_object.Text( + Text( id=20, name="name: text - invisible", text="Hello World", @@ -840,7 +850,7 @@ TEXTS = [ "y":78.4572581561896 } """, - tiled_object.Text( + Text( id=21, name="name: text - rotated", text="Hello World", @@ -874,7 +884,7 @@ TEXTS = [ "y":101.592417869728 } """, - tiled_object.Text( + Text( id=22, name="name: text - different font", text="Hello World", @@ -907,7 +917,7 @@ TEXTS = [ "y":154.192167784472 } """, - tiled_object.Text( + Text( id=23, name="name: text - no word wrap", text="Hello World", @@ -939,7 +949,7 @@ TEXTS = [ "y":1.19455496191883 } """, - tiled_object.Text( + Text( id=24, name="name: text - right bottom align", text="Hello World", @@ -973,7 +983,7 @@ TEXTS = [ "y": 3.81362964647039 } """, - tiled_object.Text( + Text( id=25, name="text: center center align", rotation=0, @@ -1006,7 +1016,7 @@ TEXTS = [ "y": 60.7785040354666 } """, - tiled_object.Text( + Text( id=26, name="name: text - justified", rotation=0, @@ -1038,7 +1048,7 @@ TEXTS = [ "y": 130.620495623508 } """, - tiled_object.Text( + Text( id=27, name="name: text - red", rotation=0, @@ -1075,7 +1085,7 @@ TEXTS = [ "y":22 } """, - tiled_object.Text( + Text( id=31, name="name: text - font options", rotation=0, @@ -1100,7 +1110,7 @@ OBJECTS = ELLIPSES + RECTANGLES + POINTS + TILES + POLYGONS + POLYLINES + TEXTS @pytest.mark.parametrize("raw_object_json,expected", OBJECTS) def test_parse_layer(raw_object_json, expected): raw_object = json.loads(raw_object_json) - result = tiled_object.cast(raw_object) + result = parse(raw_object) assert result == expected @@ -1118,4 +1128,4 @@ def test_parse_no_parent_dir(): json_object = json.loads(raw_object) with pytest.raises(RuntimeError): - tiled_object.cast(json_object) + parse(json_object) diff --git a/tests/test_tiled_object_tmx.py b/tests/test_tiled_object_tmx.py new file mode 100644 index 0000000..d277759 --- /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 diff --git a/tests/test_tileset.py b/tests/test_tileset.py index d24e9d5..9f88f78 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 import tileset +from pytiled_parser.common_types import OrderedPair, Size +from pytiled_parser.parsers.json.tileset import parse as parse_json +from pytiled_parser.parsers.tmx.tileset import parse as parse_tmx TESTS_DIR = Path(os.path.dirname(os.path.abspath(__file__))) TEST_DATA = TESTS_DIR / "test_data" @@ -26,8 +29,26 @@ ALL_TILESET_DIRS = [ ] +def fix_object(my_object): + my_object.coordinates = OrderedPair( + round(my_object.coordinates[0], 4), round(my_object.coordinates[1], 4) + ) + my_object.size = Size(round(my_object.size[0], 4), round(my_object.size[1], 4)) + + +def fix_tileset(tileset): + tileset.version = None + tileset.tiled_version = None + if tileset.tiles: + for tile in tileset.tiles.values(): + if tile.objects: + for my_object in tile.objects.tiled_objects: + fix_object(my_object) + + +@pytest.mark.parametrize("parser_type", ["json", "tmx"]) @pytest.mark.parametrize("tileset_dir", ALL_TILESET_DIRS) -def test_tilesets_integration(tileset_dir): +def test_tilesets_integration(parser_type, tileset_dir): # it's a PITA to import like this, don't do it # https://stackoverflow.com/a/67692/1342874 spec = importlib.util.spec_from_file_location( @@ -36,9 +57,16 @@ def test_tilesets_integration(tileset_dir): expected = importlib.util.module_from_spec(spec) spec.loader.exec_module(expected) - raw_tileset_path = tileset_dir / "tileset.json" + if parser_type == "json": + raw_tileset_path = tileset_dir / "tileset.json" + with open(raw_tileset_path) as raw_tileset: + tileset_ = parse_json(json.loads(raw_tileset.read()), 1) + elif parser_type == "tmx": + raw_tileset_path = tileset_dir / "tileset.tsx" + with open(raw_tileset_path) as raw_tileset: + tileset_ = parse_tmx(etree.parse(raw_tileset).getroot(), 1) - with open(raw_tileset_path) as raw_tileset: - tileset_ = tileset.cast(json.loads(raw_tileset.read()), 1) + fix_tileset(tileset_) + fix_tileset(expected.EXPECTED) assert tileset_ == expected.EXPECTED