More TMX work(it's mostly working I think)

This commit is contained in:
Darren Eberly
2021-12-17 01:09:40 -05:00
parent d653ff63a3
commit 115afb5e22
5 changed files with 562 additions and 5 deletions

View File

@@ -1,14 +1,136 @@
"""Layer parsing for the TMX Map Format.
"""
import base64
import gzip
import importlib.util
import xml.etree.ElementTree as etree
import zlib
from pathlib import Path
from typing import Optional
from typing import List, Optional
from pytiled_parser.common_types import OrderedPair, Size
from pytiled_parser.layer import ImageLayer, Layer
from pytiled_parser.layer import (
Chunk,
ImageLayer,
Layer,
LayerGroup,
ObjectLayer,
TileLayer,
)
from pytiled_parser.parsers.tmx.properties import parse as parse_properties
from pytiled_parser.parsers.tmx.tiled_object import parse as parse_object
from pytiled_parser.util import parse_color
zstd_spec = importlib.util.find_spec("zstd")
if zstd_spec:
import zstd
else:
zstd = None
def _convert_raw_tile_layer_data(data: List[int], layer_width: int) -> List[List[int]]:
"""Convert raw layer data into a nested lit based on the layer width
Args:
data: The data to convert
layer_width: Width of the layer
Returns:
List[List[int]]: A nested list containing the converted data
"""
tile_grid: List[List[int]] = [[]]
column_count = 0
row_count = 0
for item in data:
column_count += 1
tile_grid[row_count].append(item)
if not column_count % layer_width and column_count < len(data):
row_count += 1
tile_grid.append([])
return tile_grid
def _decode_tile_layer_data(
data: str, compression: str, layer_width: int
) -> List[List[int]]:
"""Decode Base64 Encoded tile data. Optionally supports gzip and zlib compression.
Args:
data: The base64 encoded data
compression: Either zlib, gzip, or empty. If empty no decompression is done.
Returns:
List[List[int]]: A nested list containing the decoded data
Raises:
ValueError: For an unsupported compression type.
"""
unencoded_data = base64.b64decode(data)
if compression == "zlib":
unzipped_data = zlib.decompress(unencoded_data)
elif compression == "gzip":
unzipped_data = gzip.decompress(unencoded_data)
elif compression == "zstd" and zstd is None:
raise ValueError(
"zstd compression support is not installed."
"To install use 'pip install pytiled-parser[zstd]'"
)
elif compression == "zstd":
unzipped_data = zstd.decompress(unencoded_data)
else:
unzipped_data = unencoded_data
tile_grid: List[int] = []
byte_count = 0
int_count = 0
int_value = 0
for byte in unzipped_data:
int_value += byte << (byte_count * 8)
byte_count += 1
if not byte_count % 4:
byte_count = 0
int_count += 1
tile_grid.append(int_value)
int_value = 0
return _convert_raw_tile_layer_data(tile_grid, layer_width)
def _parse_chunk(
raw_chunk: etree.Element,
encoding: Optional[str] = None,
compression: Optional[str] = None,
) -> Chunk:
"""Parse the raw_chunk to a Chunk.
Args:
raw_chunk: XML Element to be parsed to a Chunk
encoding: Encoding type. ("base64" or None)
compression: Either zlib, gzip, or empty. If empty no decompression is done.
Returns:
Chunk: The Chunk created from the raw_chunk
"""
if encoding == "base64":
assert isinstance(compression, str)
data = _decode_tile_layer_data(
raw_chunk.text, compression, int(raw_chunk.attrib["width"]) # type: ignore
)
else:
data = _convert_raw_tile_layer_data(
[int(v.strip) for v in raw_chunk.text], # type: ignore
int(raw_chunk.attrib["width"]),
)
return Chunk(
coordinates=OrderedPair(int(raw_chunk.attrib["x"]), int(raw_chunk.attrib["y"])),
size=Size(int(raw_chunk.attrib["width"]), int(raw_chunk.attrib["height"])),
data=data,
)
def _parse_common(raw_layer: etree.Element) -> Layer:
"""Create a Layer containing all the attributes common to all layer types.
@@ -56,6 +178,83 @@ def _parse_common(raw_layer: etree.Element) -> Layer:
return common
def _parse_tile_layer(raw_layer: etree.Element) -> TileLayer:
"""Parse the raw_layer to a TileLayer.
Args:
raw_layer: XML Element to be parsed to a TileLayer.
Returns:
TileLayer: The TileLayer created from raw_layer
"""
tile_layer = TileLayer(
size=Size(int(raw_layer.attrib["width"]), int(raw_layer.attrib["height"])),
**_parse_common(raw_layer).__dict__,
)
data_element = raw_layer.find("./data")
if data_element:
encoding = None
if data_element.attrib.get("encoding") is not None:
encoding = data_element.attrib["encoding"]
compression = ""
if data_element.attrib.get("compression") is not None:
compression = data_element.attrib["compression"]
raw_chunks = data_element.findall("./chunk")
if not raw_chunks:
if encoding:
tile_layer.data = _decode_tile_layer_data(
data=data_element.text, # type: ignore
compression=compression,
layer_width=int(raw_layer.attrib["width"]),
)
else:
tile_layer.data = _convert_raw_tile_layer_data(
[int(v.strip()) for v in data_element.text], # type: ignore
int(raw_layer.attrib["width"]),
)
else:
chunks = []
for raw_chunk in raw_chunks:
chunks.append(
_parse_chunk(
raw_chunk,
encoding,
compression,
)
)
if chunks:
tile_layer.chunks = chunks
return tile_layer
def _parse_object_layer(
raw_layer: etree.Element, parent_dir: Optional[Path] = None
) -> ObjectLayer:
"""Parse the raw_layer to an ObjectLayer.
Args:
raw_layer: XML Element to be parsed to an ObjectLayer.
Returns:
ObjectLayer: The ObjectLayer created from raw_layer
"""
objects = []
for object_ in raw_layer.findall("./object"):
objects.append(parse_object(object_, parent_dir))
return ObjectLayer(
tiled_objects=objects,
draw_order=raw_layer.attrib["draworder"],
**_parse_common(raw_layer).__dict__,
)
def _parse_image_layer(raw_layer: etree.Element) -> ImageLayer:
"""Parse the raw_layer to an ImageLayer.
@@ -85,6 +284,26 @@ def _parse_image_layer(raw_layer: etree.Element) -> ImageLayer:
raise RuntimeError("Tried to parse an image layer that doesn't have an image!")
def _parse_group_layer(
raw_layer: etree.Element, parent_dir: Optional[Path] = None
) -> LayerGroup:
"""Parse the raw_layer to a LayerGroup.
Args:
raw_layer: XML Element to be parsed to a LayerGroup.
Returns:
LayerGroup: The LayerGroup created from raw_layer
"""
layers = []
for layer in raw_layer.iter():
if layer.tag in ["layer", "objectgroup", "imagelayer", "group"]:
layers.append(parse(layer, parent_dir=parent_dir))
return LayerGroup(layers=layers, **_parse_common(raw_layer).__dict__)
def parse(
raw_layer: etree.Element,
parent_dir: Optional[Path] = None,

View File

@@ -2,8 +2,11 @@ import xml.etree.ElementTree as etree
from pathlib import Path
from pytiled_parser.common_types import OrderedPair, Size
from pytiled_parser.parsers.tmx.layer import parse as parse_layer
from pytiled_parser.parsers.tmx.properties import parse as parse_properties
from pytiled_parser.parsers.tmx.tileset import parse as parse_tileset
from pytiled_parser.tiled_map import TiledMap, TilesetDict
from pytiled_parser.util import parse_color
def parse(file: Path) -> TiledMap:
@@ -41,10 +44,15 @@ def parse(file: Path) -> TiledMap:
raw_tileset, int(raw_tileset.attrib["firstgid"])
)
layers = []
for element in raw_map.iter():
if element.tag in ["layer", "objectgroup", "imagelayer", "group"]:
layers.append(parse_layer(element, parent_dir))
map_ = TiledMap(
map_file=file,
infinite=bool(int(raw_map.attrib["infinite"])),
layers=[parse_layer(layer_, parent_dir) for layer_ in raw_tiled_map["layers"]],
layers=layers,
map_size=Size(int(raw_map.attrib["width"]), int(raw_map.attrib["height"])),
next_layer_id=int(raw_map.attrib["nextlayerid"]),
next_object_id=int(raw_map.attrib["nextobjectid"]),
@@ -57,3 +65,52 @@ def parse(file: Path) -> TiledMap:
tilesets=tilesets,
version=raw_map.attrib["version"],
)
layers = [layer for layer in map_.layers if hasattr(layer, "tiled_objects")]
for my_layer in layers:
for tiled_object in my_layer.tiled_objects:
if hasattr(tiled_object, "new_tileset"):
if tiled_object.new_tileset:
already_loaded = None
for val in map_.tilesets.values():
if val.name == tiled_object.new_tileset["name"]:
already_loaded = val
break
if not already_loaded:
highest_firstgid = max(map_.tilesets.keys())
last_tileset_count = map_.tilesets[highest_firstgid].tile_count
new_firstgid = highest_firstgid + last_tileset_count
map_.tilesets[new_firstgid] = parse_tileset(
tiled_object.new_tileset,
new_firstgid,
tiled_object.new_tileset_path,
)
tiled_object.gid = tiled_object.gid + (new_firstgid - 1)
else:
tiled_object.gid = tiled_object.gid + (
already_loaded.firstgid - 1
)
tiled_object.new_tileset = None
tiled_object.new_tileset_path = None
if raw_map.attrib.get("backgroundcolor") is not None:
map_.background_color = parse_color(raw_map.attrib["backgroundcolor"])
if raw_map.attrib.get("hexsidelength") is not None:
map_.hex_side_length = int(raw_map.attrib["hexsidelength"])
properties_element = raw_map.find("./properties")
if properties_element:
map_.properties = parse_properties(properties_element)
if raw_map.attrib.get("staggeraxis") is not None:
map_.stagger_axis = raw_map.attrib["staggeraxis"]
if raw_map.attrib.get("staggerindex") is not None:
map_.stagger_index = raw_map.attrib["staggerindex"]
return map_

View File

@@ -0,0 +1,275 @@
import xml.etree.ElementTree as etree
from pathlib import Path
from typing import Callable, Optional
from pytiled_parser.common_types import OrderedPair, Size
from pytiled_parser.parsers.tmx.properties import parse as parse_properties
from pytiled_parser.tiled_object import (
Ellipse,
Point,
Polygon,
Polyline,
Rectangle,
Text,
Tile,
TiledObject,
)
from pytiled_parser.util import parse_color
def _parse_common(raw_object: etree.Element) -> TiledObject:
"""Create an Object containing all the attributes common to all types of objects.
Args:
raw_object: XML Element to get common attributes from
Returns:
Object: The attributes in common of all types of objects
"""
common = TiledObject(
id=int(raw_object.attrib["id"]),
coordinates=OrderedPair(
float(raw_object.attrib["x"]), float(raw_object.attrib["y"])
),
visible=bool(int(raw_object.attrib["visible"])),
size=Size(
float(raw_object.attrib["width"]), float(raw_object.attrib["height"])
),
rotation=float(raw_object.attrib["rotation"]),
name=raw_object.attrib["name"],
type=raw_object.attrib["type"],
)
properties_element = raw_object.find("./properties")
if properties_element:
common.properties = parse_properties(properties_element)
return common
def _parse_ellipse(raw_object: etree.Element) -> Ellipse:
"""Parse the raw object into an Ellipse.
Args:
raw_object: XML Element to be parsed to an Ellipse
Returns:
Ellipse: The Ellipse object created from the raw object
"""
return Ellipse(**_parse_common(raw_object).__dict__)
def _parse_rectangle(raw_object: etree.Element) -> Rectangle:
"""Parse the raw object into a Rectangle.
Args:
raw_object: XML Element to be parsed to a Rectangle
Returns:
Rectangle: The Rectangle object created from the raw object
"""
return Rectangle(**_parse_common(raw_object).__dict__)
def _parse_point(raw_object: etree.Element) -> Point:
"""Parse the raw object into a Point.
Args:
raw_object: XML Element to be parsed to a Point
Returns:
Point: The Point object created from the raw object
"""
return Point(**_parse_common(raw_object).__dict__)
def _parse_polygon(raw_object: etree.Element) -> Polygon:
"""Parse the raw object into a Polygon.
Args:
raw_object: XML Element to be parsed to a Polygon
Returns:
Polygon: The Polygon object created from the raw object
"""
polygon = []
for raw_point in raw_object.attrib["points"].split(" "):
point = raw_point.split(",")
polygon.append(OrderedPair(float(point[0]), float(point[1])))
return Polygon(points=polygon, **_parse_common(raw_object).__dict__)
def _parse_polyline(raw_object: etree.Element) -> Polyline:
"""Parse the raw object into a Polyline.
Args:
raw_object: Raw object to be parsed to a Polyline
Returns:
Polyline: The Polyline object created from the raw object
"""
polyline = []
for raw_point in raw_object.attrib["polyline"].split(" "):
point = raw_point.split(",")
polyline.append(OrderedPair(float(point[0]), float(point[1])))
return Polyline(points=polyline, **_parse_common(raw_object).__dict__)
def _parse_tile(
raw_object: etree.Element,
new_tileset: Optional[etree.Element] = None,
new_tileset_path: Optional[Path] = None,
) -> Tile:
"""Parse the raw object into a Tile.
Args:
raw_object: XML Element to be parsed to a Tile
Returns:
Tile: The Tile object created from the raw object
"""
return Tile(
gid=int(raw_object.attrib["gid"]),
new_tileset=new_tileset,
new_tileset_path=new_tileset_path,
**_parse_common(raw_object).__dict__
)
def _parse_text(raw_object: etree.Element) -> Text:
"""Parse the raw object into Text.
Args:
raw_object: XML Element to be parsed to a Text
Returns:
Text: The Text object created from the raw object
"""
# required attributes
text = raw_object.text
if not text:
text = ""
# create base Text object
text_object = Text(text=text, **_parse_common(raw_object).__dict__)
# optional attributes
if raw_object.attrib.get("color") is not None:
text_object.color = parse_color(raw_object.attrib["color"])
if raw_object.attrib.get("fontfamily") is not None:
text_object.font_family = raw_object.attrib["fontfamily"]
if raw_object.attrib.get("pixelsize") is not None:
text_object.font_size = float(raw_object.attrib["pixelsize"])
if raw_object.attrib.get("bold") is not None:
text_object.bold = bool(int(raw_object.attrib["bold"]))
if raw_object.attrib.get("italic") is not None:
text_object.italic = bool(int(raw_object.attrib["italic"]))
if raw_object.attrib.get("kerning") is not None:
text_object.kerning = bool(int(raw_object.attrib["kerning"]))
if raw_object.attrib.get("strikeout") is not None:
text_object.strike_out = bool(int(raw_object.attrib["strikeout"]))
if raw_object.attrib.get("underline") is not None:
text_object.underline = bool(int(raw_object.attrib["underline"]))
if raw_object.attrib.get("halign") is not None:
text_object.horizontal_align = raw_object.attrib["halign"]
if raw_object.attrib.get("valign") is not None:
text_object.vertical_align = raw_object.attrib["valign"]
if raw_object.attrib.get("wrap") is not None:
text_object.wrap = bool(int(raw_object.attrib["wrap"]))
return text_object
def _get_parser(raw_object: etree.Element) -> Callable[[etree.Element], TiledObject]:
"""Get the parser function for a given raw object.
Only used internally by the TMX parser.
Args:
raw_object: XML Element that is analyzed to determine the parser function.
Returns:
Callable[[Element], Object]: The parser function.
"""
if raw_object.find("./ellipse"):
return _parse_ellipse
if raw_object.find("./point"):
return _parse_point
if raw_object.attrib.get("gid"):
# Only tile objects have the `gid` attribute
return _parse_tile
if raw_object.find("./polygon"):
return _parse_polygon
if raw_object.find("./polyline"):
return _parse_polyline
if raw_object.find("./text"):
return _parse_text
# If it's none of the above, rectangle is the only one left.
# Rectangle is the only object which has no properties to signify that.
return _parse_rectangle
def parse(raw_object: etree.Element, parent_dir: Optional[Path] = None) -> TiledObject:
"""Parse the raw object into a pytiled_parser version
Args:
raw_object: XML Element that is to be parsed.
parent_dir: The parent directory that the map file is in.
Returns:
TiledObject: A parsed Object.
Raises:
RuntimeError: When a parameter that is conditionally required was not sent.
"""
new_tileset = None
new_tileset_path = None
if raw_object.attrib.get("template"):
if not parent_dir:
raise RuntimeError(
"A parent directory must be specified when using object templates."
)
template_path = Path(parent_dir / raw_object.attrib["template"])
with open(template_path) as template_file:
template = etree.parse(template_file).getroot()
tileset_element = template.find("./tileset")
if tileset_element:
tileset_path = Path(
template_path.parent / tileset_element.attrib["source"]
)
with open(tileset_path) as tileset_file:
new_tileset = etree.parse(tileset_file).getroot()
new_tileset_path = tileset_path.parent
new_object = template.find("./object")
if raw_object.attrib.get("id") and new_object:
new_object.attrib["id"] = raw_object.attrib["id"]
if new_object:
raw_object = new_object
if raw_object.attrib.get("gid"):
return _parse_tile(raw_object, new_tileset, new_tileset_path)
return _get_parser(raw_object)(raw_object)

View File

@@ -3,6 +3,7 @@ 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
@@ -83,6 +84,10 @@ def _parse_tile(raw_tile: etree.Element, external_path: Optional[Path] = None) -
for raw_frame in animation_element.findall("./frame"):
tile.animation.append(_parse_frame(raw_frame))
object_element = raw_tile.find("./objectgroup")
if object_element:
tile.objects = parse_layer(object_element)
properties_element = raw_tile.find("./properties")
if properties_element:
tile.properties = parse_properties(properties_element)

View File

@@ -1,6 +1,7 @@
# pylint: disable=too-few-public-methods
import xml.etree.ElementTree as etree
from pathlib import Path
from typing import Any, Dict, List, Optional
from typing import Any, Dict, List, Optional, Union
import attr
@@ -145,5 +146,5 @@ class Tile(TiledObject):
"""
gid: int
new_tileset: Optional[Dict[str, Any]] = None
new_tileset: Optional[Union[etree.Element, Dict[str, Any]]] = None
new_tileset_path: Optional[Path] = None