From d653ff63a3705ef7388361de8d55f5f7ded538ed Mon Sep 17 00:00:00 2001 From: Darren Eberly Date: Wed, 15 Dec 2021 20:59:25 -0500 Subject: [PATCH] Work on TMX parser. It is very not done --- pytiled_parser/__init__.py | 2 +- pytiled_parser/parser.py | 5 + pytiled_parser/parsers/tmx/layer.py | 117 ++++++++++++++ pytiled_parser/parsers/tmx/properties.py | 32 ++++ pytiled_parser/parsers/tmx/tiled_map.py | 59 +++++++ pytiled_parser/parsers/tmx/tileset.py | 186 +++++++++++++++++++++++ pytiled_parser/parsers/tmx/wang_set.py | 74 +++++++++ 7 files changed, 474 insertions(+), 1 deletion(-) create mode 100644 pytiled_parser/parsers/tmx/layer.py create mode 100644 pytiled_parser/parsers/tmx/properties.py create mode 100644 pytiled_parser/parsers/tmx/tiled_map.py create mode 100644 pytiled_parser/parsers/tmx/tileset.py create mode 100644 pytiled_parser/parsers/tmx/wang_set.py diff --git a/pytiled_parser/__init__.py b/pytiled_parser/__init__.py index a58b02f..8d616bb 100644 --- a/pytiled_parser/__init__.py +++ b/pytiled_parser/__init__.py @@ -13,7 +13,7 @@ PyTiled Parser is not tied to any particular graphics library or game engine. from .common_types import OrderedPair, Size from .layer import ImageLayer, Layer, LayerGroup, ObjectLayer, TileLayer -from .parser import parse_map +from .parser import parse_map, parse_tmx from .properties import Properties from .tiled_map import TiledMap from .tileset import Tile, Tileset diff --git a/pytiled_parser/parser.py b/pytiled_parser/parser.py index 7cdd443..69343f8 100644 --- a/pytiled_parser/parser.py +++ b/pytiled_parser/parser.py @@ -1,6 +1,7 @@ from pathlib import Path from pytiled_parser.parsers.json.tiled_map import parse as json_map_parse +from pytiled_parser.parsers.tmx.tiled_map import parse as tmx_map_parse from pytiled_parser.tiled_map import TiledMap @@ -15,3 +16,7 @@ def parse_map(file: Path) -> TiledMap: """ # I have no idea why, but mypy thinks this function returns "Any" return json_map_parse(file) # type: ignore + + +def parse_tmx(file: Path) -> TiledMap: + return tmx_map_parse(file) # type: ignore diff --git a/pytiled_parser/parsers/tmx/layer.py b/pytiled_parser/parsers/tmx/layer.py new file mode 100644 index 0000000..21da21a --- /dev/null +++ b/pytiled_parser/parsers/tmx/layer.py @@ -0,0 +1,117 @@ +"""Layer parsing for the TMX Map Format. +""" +import xml.etree.ElementTree as etree +from pathlib import Path +from typing import Optional + +from pytiled_parser.common_types import OrderedPair, Size +from pytiled_parser.layer import ImageLayer, Layer +from pytiled_parser.parsers.tmx.properties import parse as parse_properties +from pytiled_parser.util import parse_color + + +def _parse_common(raw_layer: etree.Element) -> Layer: + """Create a Layer containing all the attributes common to all layer types. + + This is to create the stub Layer object that can then be used to create the actual + specific sub-classes of Layer. + + Args: + raw_layer: XML Element to get common attributes from + + Returns: + Layer: The attributes in common of all layer types + """ + common = Layer( + name=raw_layer.attrib["name"], + opacity=float(raw_layer.attrib["opacity"]), + visible=bool(int(raw_layer.attrib["visible"])), + ) + + if raw_layer.attrib.get("id") is not None: + common.id = int(raw_layer.attrib["id"]) + + if raw_layer.attrib.get("offsetx") is not None: + common.offset = OrderedPair( + float(raw_layer.attrib["offsetx"]), float(raw_layer.attrib["offsety"]) + ) + + properties_element = raw_layer.find("./properties") + if properties_element: + common.properties = parse_properties(properties_element) + + parallax = [1.0, 1.0] + + if raw_layer.attrib.get("parallaxx") is not None: + parallax[0] = float(raw_layer.attrib["parallaxx"]) + + if raw_layer.attrib.get("parallaxy") is not None: + parallax[1] = float(raw_layer.attrib["parallaxy"]) + + common.parallax_factor = OrderedPair(parallax[0], parallax[1]) + + if raw_layer.attrib.get("tintcolor") is not None: + common.tint_color = parse_color(raw_layer.attrib["tintcolor"]) + + return common + + +def _parse_image_layer(raw_layer: etree.Element) -> ImageLayer: + """Parse the raw_layer to an ImageLayer. + + Args: + raw_layer: XML Element to be parsed to an ImageLayer. + + Returns: + ImageLayer: The ImageLayer created from raw_layer + """ + image_element = raw_layer.find("./image") + if image_element: + source = Path(image_element.attrib["source"]) + width = int(image_element.attrib["width"]) + height = int(image_element.attrib["height"]) + + transparent_color = None + if image_element.attrib.get("trans") is not None: + transparent_color = parse_color(image_element.attrib["trans"]) + + return ImageLayer( + image=source, + size=Size(width, height), + transparent_color=transparent_color, + **_parse_common(raw_layer).__dict__, + ) + + raise RuntimeError("Tried to parse an image layer that doesn't have an image!") + + +def parse( + raw_layer: etree.Element, + parent_dir: Optional[Path] = None, +) -> Layer: + """Parse a raw Layer into a pytiled_parser object. + + This function will determine the type of layer and parse accordingly. + + Args: + raw_layer: Raw layer to be parsed. + parent_dir: The parent directory that the map file is in. + + Returns: + Layer: A parsed Layer. + + Raises: + RuntimeError: For an invalid layer type being provided + """ + type_ = raw_layer.tag + + if type_ == "objectgroup": + return _parse_object_layer(raw_layer, parent_dir) + elif type_ == "group": + return _parse_group_layer(raw_layer, parent_dir) + elif type_ == "imagelayer": + return _parse_image_layer(raw_layer) + elif type_ == "layer": + return _parse_tile_layer(raw_layer) + + raise RuntimeError(f"An invalid layer type of {type_} was supplied") diff --git a/pytiled_parser/parsers/tmx/properties.py b/pytiled_parser/parsers/tmx/properties.py new file mode 100644 index 0000000..b831a17 --- /dev/null +++ b/pytiled_parser/parsers/tmx/properties.py @@ -0,0 +1,32 @@ +import xml.etree.ElementTree as etree +from pathlib import Path +from typing import List, Union, cast + +from pytiled_parser.properties import Properties, Property +from pytiled_parser.util import parse_color + + +def parse(raw_properties: etree.Element) -> Properties: + + final: Properties = {} + value: Property + + for raw_property in raw_properties.findall("./property"): + type_ = raw_property.attrib["type"] + value_ = raw_property.attrib["value"] + if type_ == "file": + value = Path(value_) + elif type_ == "color": + value = parse_color(value_) + elif type_ == "int" or type_ == "float": + value = float(value_) + elif type_ == "bool": + if value_ == "true": + value = True + else: + value = False + else: + value = value_ + final[raw_property.attrib["name"]] = value + + return final diff --git a/pytiled_parser/parsers/tmx/tiled_map.py b/pytiled_parser/parsers/tmx/tiled_map.py new file mode 100644 index 0000000..a744495 --- /dev/null +++ b/pytiled_parser/parsers/tmx/tiled_map.py @@ -0,0 +1,59 @@ +import xml.etree.ElementTree as etree +from pathlib import Path + +from pytiled_parser.common_types import OrderedPair, Size +from pytiled_parser.parsers.tmx.tileset import parse as parse_tileset +from pytiled_parser.tiled_map import TiledMap, TilesetDict + + +def parse(file: Path) -> TiledMap: + """Parse the raw Tiled map into a pytiled_parser type. + + Args: + file: Path to the map file. + + Returns: + TiledMap: A parsed TiledMap. + """ + with open(file) as map_file: + raw_map = etree.parse(map_file).getroot() + + parent_dir = file.parent + + raw_tilesets = raw_map.findall("./tileset") + tilesets: TilesetDict = {} + + for raw_tileset in raw_tilesets: + if raw_tileset.attrib.get("source") is not None: + # Is an external Tileset + tileset_path = Path(parent_dir / raw_tileset.attrib["source"]) + with open(tileset_path) as tileset_file: + raw_tileset = etree.parse(tileset_file).getroot() + + tilesets[int(raw_tileset.attrib["firstgid"])] = parse_tileset( + raw_tileset, + int(raw_tileset.attrib["firstgid"]), + external_path=tileset_path.parent, + ) + else: + # Is an embedded Tileset + tilesets[int(raw_tileset.attrib["firstgid"])] = parse_tileset( + raw_tileset, int(raw_tileset.attrib["firstgid"]) + ) + + map_ = TiledMap( + map_file=file, + infinite=bool(int(raw_map.attrib["infinite"])), + layers=[parse_layer(layer_, parent_dir) for layer_ in raw_tiled_map["layers"]], + map_size=Size(int(raw_map.attrib["width"]), int(raw_map.attrib["height"])), + next_layer_id=int(raw_map.attrib["nextlayerid"]), + next_object_id=int(raw_map.attrib["nextobjectid"]), + orientation=raw_map.attrib["orientation"], + render_order=raw_map.attrib["renderorder"], + tiled_version=raw_map.attrib["tiledversion"], + tile_size=Size( + int(raw_map.attrib["tilewidth"]), int(raw_map.attrib["tileheight"]) + ), + tilesets=tilesets, + version=raw_map.attrib["version"], + ) diff --git a/pytiled_parser/parsers/tmx/tileset.py b/pytiled_parser/parsers/tmx/tileset.py new file mode 100644 index 0000000..3a123f7 --- /dev/null +++ b/pytiled_parser/parsers/tmx/tileset.py @@ -0,0 +1,186 @@ +import xml.etree.ElementTree as etree +from pathlib import Path +from typing import Optional + +from pytiled_parser.common_types import OrderedPair +from pytiled_parser.parsers.tmx.properties import parse as parse_properties +from pytiled_parser.parsers.tmx.wang_set import parse as parse_wangset +from pytiled_parser.tileset import Frame, Grid, Tile, Tileset, Transformations +from pytiled_parser.util import parse_color + + +def _parse_frame(raw_frame: etree.Element) -> Frame: + """Parse the raw_frame to a Frame object. + + Args: + raw_frame: XML Element to be parsed to a Frame + + Returns: + Frame: The Frame created from the raw_frame + """ + + return Frame( + duration=int(raw_frame.attrib["duration"]), + tile_id=int(raw_frame.attrib["tileid"]), + ) + + +def _parse_grid(raw_grid: etree.Element) -> Grid: + """Parse the raw_grid to a Grid object. + + Args: + raw_grid: XML Element to be parsed to a Grid + + Returns: + Grid: The Grid created from the raw_grid + """ + + return Grid( + orientation=raw_grid.attrib["orientation"], + width=int(raw_grid.attrib["width"]), + height=int(raw_grid.attrib["height"]), + ) + + +def _parse_transformations(raw_transformations: etree.Element) -> Transformations: + """Parse the raw_transformations to a Transformations object. + + Args: + raw_transformations: XML Element to be parsed to a Transformations + + Returns: + Transformations: The Transformations created from the raw_transformations + """ + + return Transformations( + hflip=bool(int(raw_transformations.attrib["hflip"])), + vflip=bool(int(raw_transformations.attrib["vflip"])), + rotate=bool(int(raw_transformations.attrib["rotate"])), + prefer_untransformed=bool( + int(raw_transformations.attrib["preferuntransformed"]) + ), + ) + + +def _parse_tile(raw_tile: etree.Element, external_path: Optional[Path] = None) -> Tile: + """Parse the raw_tile to a Tile object. + + Args: + raw_tile: XML Element to be parsed to a Tile + + Returns: + Tile: The Tile created from the raw_tile + """ + + tile = Tile(id=int(raw_tile.attrib["id"])) + + if raw_tile.attrib.get("type") is not None: + tile.type = raw_tile.attrib["type"] + + animation_element = raw_tile.find("./animation") + if animation_element: + tile.animation = [] + for raw_frame in animation_element.findall("./frame"): + tile.animation.append(_parse_frame(raw_frame)) + + properties_element = raw_tile.find("./properties") + if properties_element: + tile.properties = parse_properties(properties_element) + + image_element = raw_tile.find("./image") + if image_element: + if external_path: + tile.image = ( + Path(external_path / image_element.attrib["source"]) + .absolute() + .resolve() + ) + else: + tile.image = Path(image_element.attrib["source"]) + + tile.image_width = int(image_element.attrib["width"]) + tile.image_height = int(image_element.attrib["height"]) + + return tile + + +def parse( + raw_tileset: etree.Element, + firstgid: int, + external_path: Optional[Path] = None, +) -> Tileset: + tileset = Tileset( + name=raw_tileset.attrib["name"], + tile_count=int(raw_tileset.attrib["tilecount"]), + tile_width=int(raw_tileset.attrib["tilewidth"]), + tile_height=int(raw_tileset.attrib["tileheight"]), + columns=int(raw_tileset.attrib["columns"]), + spacing=int(raw_tileset.attrib["spacing"]), + margin=int(raw_tileset.attrib["margin"]), + firstgid=firstgid, + ) + + if raw_tileset.attrib.get("version") is not None: + tileset.version = raw_tileset.attrib["version"] + + if raw_tileset.attrib.get("tiledversion") is not None: + tileset.tiled_version = raw_tileset.attrib["tiledversion"] + + if raw_tileset.attrib.get("backgroundcolor") is not None: + tileset.background_color = parse_color(raw_tileset.attrib["backgroundcolor"]) + + image_element = raw_tileset.find("./image") + if image_element: + if external_path: + tileset.image = ( + Path(external_path / image_element.attrib["source"]) + .absolute() + .resolve() + ) + else: + tileset.image = Path(image_element.attrib["source"]) + + tileset.image_width = int(image_element.attrib["width"]) + tileset.image_height = int(image_element.attrib["height"]) + + if image_element.attrib.get("trans") is not None: + my_string = image_element.attrib["trans"] + if my_string[0] != "#": + my_string = f"#{my_string}" + tileset.transparent_color = parse_color(my_string) + pass + + tileoffset_element = raw_tileset.find("./tileoffset") + if tileoffset_element: + tileset.tile_offset = OrderedPair( + int(tileoffset_element.attrib["x"]), int(tileoffset_element.attrib["y"]) + ) + + grid_element = raw_tileset.find("./grid") + if grid_element: + tileset.grid = _parse_grid(grid_element) + + properties_element = raw_tileset.find("./properties") + if properties_element: + tileset.properties = parse_properties(properties_element) + + tiles = {} + for tile_element in raw_tileset.findall("./tiles"): + tiles[int(tile_element.attrib["id"])] = _parse_tile( + tile_element, external_path=external_path + ) + if tiles: + tileset.tiles = tiles + + wangsets_element = raw_tileset.find("./wangsets") + if wangsets_element: + wangsets = [] + for raw_wangset in wangsets_element.findall("./wangset"): + wangsets.append(parse_wangset(raw_wangset)) + tileset.wang_sets = wangsets + + transformations_element = raw_tileset.find("./transformations") + if transformations_element: + tileset.transformations = _parse_transformations(transformations_element) + + return tileset diff --git a/pytiled_parser/parsers/tmx/wang_set.py b/pytiled_parser/parsers/tmx/wang_set.py new file mode 100644 index 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