mirror of
https://github.com/OMGeeky/pytiled_parser.git
synced 2026-01-21 01:51:42 +01:00
@@ -1,4 +1,4 @@
|
|||||||
from . import utilities
|
from . import utilities
|
||||||
from . import objects
|
from . import objects
|
||||||
|
|
||||||
from .parser import parse_tile_map
|
from .xml_parser import parse_tile_map
|
||||||
|
|||||||
@@ -11,30 +11,11 @@ from pathlib import Path
|
|||||||
|
|
||||||
import xml.etree.ElementTree as etree
|
import xml.etree.ElementTree as etree
|
||||||
|
|
||||||
from typing import * # pylint: disable=W0401
|
from typing import NamedTuple, Union, Optional, List, Dict
|
||||||
|
|
||||||
|
|
||||||
class EncodingError(Exception):
|
|
||||||
"""
|
|
||||||
Tmx layer encoding is of an unknown type.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class TileNotFoundError(Exception):
|
|
||||||
"""
|
|
||||||
Tile not found in tileset.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class ImageNotFoundError(Exception):
|
|
||||||
"""
|
|
||||||
Image not found.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class Color(NamedTuple):
|
class Color(NamedTuple):
|
||||||
"""
|
"""Color object.
|
||||||
Color object.
|
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
:red (int): Red, between 1 and 255.
|
:red (int): Red, between 1 and 255.
|
||||||
@@ -42,6 +23,7 @@ class Color(NamedTuple):
|
|||||||
:blue (int): Blue, between 1 and 255.
|
:blue (int): Blue, between 1 and 255.
|
||||||
:alpha (int): Alpha, between 1 and 255.
|
:alpha (int): Alpha, between 1 and 255.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
red: int
|
red: int
|
||||||
green: int
|
green: int
|
||||||
blue: int
|
blue: int
|
||||||
@@ -49,17 +31,29 @@ class Color(NamedTuple):
|
|||||||
|
|
||||||
|
|
||||||
class OrderedPair(NamedTuple):
|
class OrderedPair(NamedTuple):
|
||||||
"""
|
"""OrderedPair NamedTuple.
|
||||||
OrderedPair NamedTuple.
|
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
:x (Union[int, float]): X coordinate.
|
x (Union[int, float]): X coordinate.
|
||||||
:y (Union[int, float]): Y coordinate.
|
y (Union[int, float]): Y coordinate.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
x: Union[int, float]
|
x: Union[int, float]
|
||||||
y: Union[int, float]
|
y: Union[int, float]
|
||||||
|
|
||||||
|
|
||||||
|
class Size(NamedTuple):
|
||||||
|
"""Size NamedTuple.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
width (Union[int, float]): The width of the object.
|
||||||
|
size (Union[int, float]): The height of the object.
|
||||||
|
"""
|
||||||
|
|
||||||
|
width: Union[int, float]
|
||||||
|
height: Union[int, float]
|
||||||
|
|
||||||
|
|
||||||
class Template:
|
class Template:
|
||||||
"""
|
"""
|
||||||
FIXME TODO
|
FIXME TODO
|
||||||
@@ -80,13 +74,15 @@ class Chunk:
|
|||||||
:layer_data (List[List(int)]): The global tile IDs in chunky
|
:layer_data (List[List(int)]): The global tile IDs in chunky
|
||||||
according to row.
|
according to row.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
location: OrderedPair
|
location: OrderedPair
|
||||||
width: int
|
width: int
|
||||||
height: int
|
height: int
|
||||||
chunk_data: List[List[int]]
|
chunk_data: List[List[int]]
|
||||||
|
|
||||||
|
|
||||||
class Image(NamedTuple):
|
@dataclasses.dataclass
|
||||||
|
class Image:
|
||||||
"""
|
"""
|
||||||
Image object.
|
Image object.
|
||||||
|
|
||||||
@@ -101,9 +97,10 @@ class Image(NamedTuple):
|
|||||||
(optional, used for tile index correction when the image changes).
|
(optional, used for tile index correction when the image changes).
|
||||||
:height (Optional[str]): The image height in pixels (optional).
|
:height (Optional[str]): The image height in pixels (optional).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
source: str
|
source: str
|
||||||
size: OrderedPair
|
size: Optional[Size] = None
|
||||||
trans: Optional[Color]
|
trans: Optional[Color] = None
|
||||||
|
|
||||||
|
|
||||||
Properties = Dict[str, Union[int, float, Color, Path, str]]
|
Properties = Dict[str, Union[int, float, Color, Path, str]]
|
||||||
@@ -117,6 +114,7 @@ class Grid(NamedTuple):
|
|||||||
determines how tile overlays for terrain and collision information
|
determines how tile overlays for terrain and collision information
|
||||||
are rendered.
|
are rendered.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
orientation: str
|
orientation: str
|
||||||
width: int
|
width: int
|
||||||
height: int
|
height: int
|
||||||
@@ -131,6 +129,7 @@ class Terrain(NamedTuple):
|
|||||||
:tile (int): The local tile-id of the tile that represents the
|
:tile (int): The local tile-id of the tile that represents the
|
||||||
terrain visually.
|
terrain visually.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name: str
|
name: str
|
||||||
tile: int
|
tile: int
|
||||||
|
|
||||||
@@ -147,6 +146,7 @@ class Frame(NamedTuple):
|
|||||||
:duration (int): How long in milliseconds this frame should be
|
:duration (int): How long in milliseconds this frame should be
|
||||||
displayed before advancing to the next frame.
|
displayed before advancing to the next frame.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
tile_id: int
|
tile_id: int
|
||||||
duration: int
|
duration: int
|
||||||
|
|
||||||
@@ -165,6 +165,7 @@ class TileTerrain:
|
|||||||
:bottom_left (Optional[int]): Bottom left terrain type.
|
:bottom_left (Optional[int]): Bottom left terrain type.
|
||||||
:bottom_right (Optional[int]): Bottom right terrain type.
|
:bottom_right (Optional[int]): Bottom right terrain type.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
top_left: Optional[int] = None
|
top_left: Optional[int] = None
|
||||||
top_right: Optional[int] = None
|
top_right: Optional[int] = None
|
||||||
bottom_left: Optional[int] = None
|
bottom_left: Optional[int] = None
|
||||||
@@ -172,44 +173,35 @@ class TileTerrain:
|
|||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
class _LayerTypeBase:
|
class Layer:
|
||||||
id: int # pylint: disable=C0103
|
"""Class that all layers inherret from.
|
||||||
name: str
|
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
|
||||||
class _LayerTypeDefaults:
|
|
||||||
offset: OrderedPair = OrderedPair(0, 0)
|
|
||||||
opacity: int = 0xFF
|
|
||||||
|
|
||||||
properties: Optional[Properties] = None
|
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
|
||||||
class LayerType(_LayerTypeDefaults, _LayerTypeBase):
|
|
||||||
"""
|
|
||||||
Class that all layer classes inherit from.
|
|
||||||
|
|
||||||
Not to be directly used.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
:layer_element (etree.Element): Element to be parsed into a
|
id: Unique ID of the layer. Each layer that added to a map gets a
|
||||||
LayerType object.
|
unique id. Even if a layer is deleted, no layer ever gets the same
|
||||||
|
ID.
|
||||||
Attributes:
|
name: The name of the layer object.
|
||||||
:id (int): Unique ID of the layer. Each layer that added to a map
|
tiled_objects: List of tiled_objects in the layer.
|
||||||
gets a unique id. Even if a layer is deleted, no layer ever gets
|
offset: Rendering offset of the layer object in pixels.
|
||||||
the same ID.
|
opacity: Decimal value between 0 and 1 to determine opacity. 1 is
|
||||||
:name (Optional[str):] The name of the layer object.
|
completely opaque, 0 is completely transparent.
|
||||||
:offset (OrderedPair): Rendering offset of the layer object in
|
properties: Properties for the layer.
|
||||||
pixels. (default: (0, 0).
|
color: The color used to display the objects in this group.
|
||||||
:opacity (int): Value between 0 and 255 to determine opacity. NOTE:
|
FIXME: editor only?
|
||||||
this value is converted from a float provided by Tiled, so some
|
draworder: Whether the objects are drawn according to the order of the
|
||||||
precision is lost.
|
object elements in the object group element ('manual'), or sorted
|
||||||
:properties (Optional[Properties]): Properties object for layer
|
by their y-coordinate ('topdown'). Defaults to 'topdown'. See:
|
||||||
object.
|
https://doc.mapeditor.org/en/stable/manual/objects/#changing-stacking-order
|
||||||
|
for more info.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
id: int
|
||||||
|
name: str
|
||||||
|
|
||||||
|
offset: Optional[OrderedPair]
|
||||||
|
opacity: Optional[float]
|
||||||
|
properties: Optional[Properties]
|
||||||
|
|
||||||
|
|
||||||
LayerData = Union[List[List[int]], List[Chunk]]
|
LayerData = Union[List[List[int]], List[Chunk]]
|
||||||
"""
|
"""
|
||||||
@@ -221,38 +213,34 @@ Either a 2 dimensional array of integers representing the global tile IDs
|
|||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
class _LayerBase:
|
class TileLayer(Layer):
|
||||||
size: OrderedPair
|
"""Tile map layer containing tiles.
|
||||||
|
|
||||||
|
See: https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#layer
|
||||||
|
|
||||||
|
Args:
|
||||||
|
size: The width of the layer in tiles. The same as the map width
|
||||||
|
unless map is infitite.
|
||||||
|
data: Either an 2 dimensional array of integers representing the
|
||||||
|
global tile IDs for the map layer, or a list of chunks for an
|
||||||
|
infinite map.
|
||||||
|
"""
|
||||||
|
|
||||||
|
size: Size
|
||||||
data: LayerData
|
data: LayerData
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
class Layer(LayerType, _LayerBase):
|
class _TiledObjectBase:
|
||||||
"""
|
|
||||||
Map layer object.
|
|
||||||
|
|
||||||
See: https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#layer
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
:size (OrderedPair): The width of the layer in tiles. Always the same
|
|
||||||
as the map width for not infitite maps.
|
|
||||||
:data (LayerData): Either an 2 dimensional array of integers
|
|
||||||
representing the global tile IDs for the map layer, or a list of
|
|
||||||
chunks for an infinite map.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
|
||||||
class _ObjectBase:
|
|
||||||
id: int
|
id: int
|
||||||
location: OrderedPair
|
location: OrderedPair
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
class _ObjectDefaults:
|
class _TiledObjectDefaults:
|
||||||
size: OrderedPair = OrderedPair(0, 0)
|
size: Size = Size(0, 0)
|
||||||
rotation: int = 0
|
rotation: int = 0
|
||||||
opacity: int = 0xFF
|
opacity: float = 1
|
||||||
|
|
||||||
name: Optional[str] = None
|
name: Optional[str] = None
|
||||||
type: Optional[str] = None
|
type: Optional[str] = None
|
||||||
@@ -262,33 +250,33 @@ class _ObjectDefaults:
|
|||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
class Object(_ObjectDefaults, _ObjectBase):
|
class TiledObject(_TiledObjectDefaults, _TiledObjectBase):
|
||||||
"""
|
"""
|
||||||
ObjectGroup Object.
|
TiledObject object.
|
||||||
|
|
||||||
See: \
|
See:
|
||||||
https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#object
|
https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#object
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
:id (int): Unique ID of the object. Each object that is placed on a
|
:id (int): Unique ID of the object. Each object that is placed on a
|
||||||
map gets a unique id. Even if an object was deleted, no object
|
map gets a unique id. Even if an object was deleted, no object
|
||||||
gets the same ID.
|
gets the same ID.
|
||||||
:location (OrderedPair): The location of the object in pixels.
|
:location (OrderedPair): The location of the object in pixels.
|
||||||
:size (OrderedPair): The width of the object in pixels
|
:size (Size): The width of the object in pixels
|
||||||
(default: (0, 0)).
|
(default: (0, 0)).
|
||||||
:rotation (int): The rotation of the object in degrees clockwise
|
:rotation (int): The rotation of the object in degrees clockwise
|
||||||
(default: 0).
|
(default: 0).
|
||||||
:opacity (int): The opacity of the object. (default: 255)
|
:opacity (int): The opacity of the object. (default: 255)
|
||||||
:name (Optional[str]): The name of the object.
|
:name (Optional[str]): The name of the object.
|
||||||
:type (Optional[str]): The type of the object.
|
:type (Optional[str]): The type of the object.
|
||||||
:properties (Properties): The properties of the Object.
|
:properties (Properties): The properties of the TiledObject.
|
||||||
:template Optional[Template]: A reference to a Template object
|
:template Optional[Template]: A reference to a Template object
|
||||||
FIXME
|
FIXME
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
class RectangleObject(Object):
|
class RectangleObject(TiledObject):
|
||||||
"""
|
"""
|
||||||
Rectangle shape defined by a point, width, and height.
|
Rectangle shape defined by a point, width, and height.
|
||||||
|
|
||||||
@@ -299,7 +287,7 @@ class RectangleObject(Object):
|
|||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
class ElipseObject(Object):
|
class ElipseObject(TiledObject):
|
||||||
"""
|
"""
|
||||||
Elipse shape defined by a point, width, and height.
|
Elipse shape defined by a point, width, and height.
|
||||||
|
|
||||||
@@ -308,7 +296,7 @@ class ElipseObject(Object):
|
|||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
class PointObject(Object):
|
class PointObject(TiledObject):
|
||||||
"""
|
"""
|
||||||
Point defined by a point (x,y).
|
Point defined by a point (x,y).
|
||||||
|
|
||||||
@@ -317,12 +305,12 @@ class PointObject(Object):
|
|||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
class _TileObjectBase(_ObjectBase):
|
class _TileImageObjectBase(_TiledObjectBase):
|
||||||
gid: int
|
gid: int
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
class TileObject(Object, _TileObjectBase):
|
class TileImageObject(TiledObject, _TileImageObjectBase):
|
||||||
"""
|
"""
|
||||||
Polygon shape defined by a set of connections between points.
|
Polygon shape defined by a set of connections between points.
|
||||||
|
|
||||||
@@ -334,12 +322,12 @@ class TileObject(Object, _TileObjectBase):
|
|||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
class _PointsObjectBase(_ObjectBase):
|
class _PointsObjectBase(_TiledObjectBase):
|
||||||
points: List[OrderedPair]
|
points: List[OrderedPair]
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
class PolygonObject(Object, _PointsObjectBase):
|
class PolygonObject(TiledObject, _PointsObjectBase):
|
||||||
"""
|
"""
|
||||||
Polygon shape defined by a set of connections between points.
|
Polygon shape defined by a set of connections between points.
|
||||||
|
|
||||||
@@ -351,12 +339,12 @@ class PolygonObject(Object, _PointsObjectBase):
|
|||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
class PolylineObject(Object, _PointsObjectBase):
|
class PolylineObject(TiledObject, _PointsObjectBase):
|
||||||
"""
|
"""
|
||||||
Polyline defined by a set of connections between points.
|
Polyline defined by a set of connections between points.
|
||||||
|
|
||||||
See: \
|
See:
|
||||||
https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#polyline
|
https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#polyline
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
:points (List[Tuple[int, int]]): List of coordinates relative to \
|
:points (List[Tuple[int, int]]): List of coordinates relative to \
|
||||||
@@ -365,27 +353,27 @@ https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#polyline
|
|||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
class _TextObjectBase(_ObjectBase):
|
class _TextObjectBase(_TiledObjectBase):
|
||||||
text: str
|
text: str
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
class _TextObjectDefaults(_ObjectDefaults):
|
class _TextObjectDefaults(_TiledObjectDefaults):
|
||||||
font_family: str = 'sans-serif'
|
font_family: str = "sans-serif"
|
||||||
font_size: int = 16
|
font_size: int = 16
|
||||||
wrap: bool = False
|
wrap: bool = False
|
||||||
color: Color = Color(0xFF, 0, 0, 0)
|
color: str = "#000000"
|
||||||
bold: bool = False
|
bold: bool = False
|
||||||
italic: bool = False
|
italic: bool = False
|
||||||
underline: bool = False
|
underline: bool = False
|
||||||
strike_out: bool = False
|
strike_out: bool = False
|
||||||
kerning: bool = False
|
kerning: bool = False
|
||||||
horizontal_align: str = 'left'
|
horizontal_align: str = "left"
|
||||||
vertical_align: str = 'top'
|
vertical_align: str = "top"
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
class TextObject(Object, _TextObjectDefaults, _TextObjectBase):
|
class TextObject(TiledObject, _TextObjectDefaults, _TextObjectBase):
|
||||||
"""
|
"""
|
||||||
Text object with associated settings.
|
Text object with associated settings.
|
||||||
|
|
||||||
@@ -410,45 +398,36 @@ class TextObject(Object, _TextObjectDefaults, _TextObjectBase):
|
|||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
class _ObjectGroupBase(_LayerTypeBase):
|
class ObjectLayer(Layer):
|
||||||
objects: List[Object]
|
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
|
||||||
class _ObjectGroupDefaults(_LayerTypeDefaults):
|
|
||||||
color: Optional[Color] = None
|
|
||||||
draw_order: Optional[str] = 'topdown'
|
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
|
||||||
class ObjectGroup(LayerType, _ObjectGroupDefaults, _ObjectGroupBase):
|
|
||||||
"""
|
"""
|
||||||
Object Group Object.
|
TiledObject Group Object.
|
||||||
|
|
||||||
The object group is in fact a map layer, and is hence called \
|
The object group is in fact a map layer, and is hence called \
|
||||||
“object layer” in Tiled.
|
“object layer” in Tiled.
|
||||||
|
|
||||||
See: \
|
See:
|
||||||
https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#objectgroup
|
https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#objectgroup
|
||||||
|
|
||||||
Attributes:
|
Args:
|
||||||
:color (Optional[Color]): The color used to display the objects
|
tiled_objects: List of tiled_objects in the layer.
|
||||||
in this group. FIXME: editor only?
|
offset: Rendering offset of the layer object in pixels.
|
||||||
:draworder (str): Whether the objects are drawn according to the
|
color: The color used to display the objects in this group.
|
||||||
order of the object elements in the object group element
|
FIXME: editor only?
|
||||||
('manual'), or sorted by their y-coordinate ('topdown'). Defaults
|
draworder: Whether the objects are drawn according to the order of the
|
||||||
to 'topdown'. See:
|
object elements in the object group element ('manual'), or sorted
|
||||||
|
by their y-coordinate ('topdown'). Defaults to 'topdown'. See:
|
||||||
https://doc.mapeditor.org/en/stable/manual/objects/#changing-stacking-order
|
https://doc.mapeditor.org/en/stable/manual/objects/#changing-stacking-order
|
||||||
for more info.
|
for more info.
|
||||||
:objects (Dict[int, Object]): Dict Object objects by Object.id.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
tiled_objects: List[TiledObject]
|
||||||
|
|
||||||
class _LayerGroupBase(_LayerTypeBase):
|
color: Optional[str] = None
|
||||||
layers: Optional[List[LayerType]]
|
draw_order: Optional[str] = "topdown"
|
||||||
|
|
||||||
|
|
||||||
class LayerGroup(LayerType):
|
@dataclasses.dataclass
|
||||||
|
class LayerGroup(Layer):
|
||||||
"""
|
"""
|
||||||
Layer Group.
|
Layer Group.
|
||||||
|
|
||||||
@@ -463,6 +442,14 @@ class LayerGroup(LayerType):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
layers: Optional[List[Union["LayerGroup", Layer, ObjectLayer]]]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class Hitbox:
|
||||||
|
"""Group of hitboxes for
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
class Tile:
|
class Tile:
|
||||||
@@ -477,12 +464,13 @@ class Tile:
|
|||||||
:animation (List[Frame]): Each tile can have exactly one animation
|
:animation (List[Frame]): Each tile can have exactly one animation
|
||||||
associated with it.
|
associated with it.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
id: int
|
id: int
|
||||||
type: Optional[str]
|
type: Optional[str]
|
||||||
terrain: Optional[TileTerrain]
|
terrain: Optional[TileTerrain]
|
||||||
animation: Optional[List[Frame]]
|
animation: Optional[List[Frame]]
|
||||||
image: Optional[Image]
|
image: Optional[Image]
|
||||||
hit_box: Optional[List[Object]]
|
hitboxes: Optional[List[TiledObject]]
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
@@ -492,7 +480,7 @@ class TileSet:
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
:name (str): The name of this tileset.
|
:name (str): The name of this tileset.
|
||||||
:max_tile_size (OrderedPair): The maximum size of a tile in this
|
:max_tile_size (Size): The maximum size of a tile in this
|
||||||
tile set in pixels.
|
tile set in pixels.
|
||||||
:spacing (int): The spacing in pixels between the tiles in this
|
:spacing (int): The spacing in pixels between the tiles in this
|
||||||
tileset (applies to the tileset image).
|
tileset (applies to the tileset image).
|
||||||
@@ -515,8 +503,9 @@ class TileSet:
|
|||||||
file.
|
file.
|
||||||
:tiles (Optional[Dict[int, Tile]]): Dict of Tile objects by Tile.id.
|
:tiles (Optional[Dict[int, Tile]]): Dict of Tile objects by Tile.id.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name: str
|
name: str
|
||||||
max_tile_size: OrderedPair
|
max_tile_size: Size
|
||||||
spacing: Optional[int]
|
spacing: Optional[int]
|
||||||
margin: Optional[int]
|
margin: Optional[int]
|
||||||
tile_count: Optional[int]
|
tile_count: Optional[int]
|
||||||
@@ -529,6 +518,9 @@ class TileSet:
|
|||||||
tiles: Optional[Dict[int, Tile]]
|
tiles: Optional[Dict[int, Tile]]
|
||||||
|
|
||||||
|
|
||||||
|
TileSetDict = Dict[int, TileSet]
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
class TileMap:
|
class TileMap:
|
||||||
"""
|
"""
|
||||||
@@ -548,8 +540,8 @@ class TileMap:
|
|||||||
rendered. Valid values are right-down, right-up, left-down and
|
rendered. Valid values are right-down, right-up, left-down and
|
||||||
left-up. In all cases, the map is drawn row-by-row. (only
|
left-up. In all cases, the map is drawn row-by-row. (only
|
||||||
supported for orthogonal maps at the moment)
|
supported for orthogonal maps at the moment)
|
||||||
:map_size (OrderedPair): The map width in tiles.
|
:map_size (Size): The map width in tiles.
|
||||||
:tile_size (OrderedPair): The width of a tile.
|
:tile_size (Size): The width of a tile.
|
||||||
:infinite (bool): If the map is infinite or not.
|
:infinite (bool): If the map is infinite or not.
|
||||||
:hexsidelength (int): Only for hexagonal maps. Determines the width or
|
:hexsidelength (int): Only for hexagonal maps. Determines the width or
|
||||||
height (depending on the staggered axis) of the tile’s edge, in
|
height (depending on the staggered axis) of the tile’s edge, in
|
||||||
@@ -563,34 +555,35 @@ class TileMap:
|
|||||||
:nextlayerid (int): Stores the next available ID for new layers.
|
:nextlayerid (int): Stores the next available ID for new layers.
|
||||||
:nextobjectid (int): Stores the next available ID for new objects.
|
:nextobjectid (int): Stores the next available ID for new objects.
|
||||||
:tile_sets (dict[str, TileSet]): Dict of tile sets used
|
:tile_sets (dict[str, TileSet]): Dict of tile sets used
|
||||||
in this map. Key is the source for external tile sets or the name
|
in this map. Key is the first GID for the tile set. The value
|
||||||
for embedded ones. The value is a TileSet object.
|
is a TileSet object.
|
||||||
:layers List[LayerType]: List of layer objects by draw order.
|
:layers List[LayerType]: List of layer objects by draw order.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
parent_dir: Path
|
parent_dir: Path
|
||||||
|
|
||||||
version: str
|
version: str
|
||||||
tiled_version: str
|
tiled_version: str
|
||||||
orientation: str
|
orientation: str
|
||||||
render_order: str
|
render_order: str
|
||||||
map_size: OrderedPair
|
map_size: Size
|
||||||
tile_size: OrderedPair
|
tile_size: Size
|
||||||
infinite: bool
|
infinite: bool
|
||||||
next_layer_id: int
|
next_layer_id: int
|
||||||
next_object_id: int
|
next_object_id: int
|
||||||
|
|
||||||
tile_sets: Dict[int, TileSet]
|
tile_sets: TileSetDict
|
||||||
layers: List[LayerType]
|
layers: List[Layer]
|
||||||
|
|
||||||
hex_side_length: Optional[int] = None
|
hex_side_length: Optional[int] = None
|
||||||
stagger_axis: Optional[int] = None
|
stagger_axis: Optional[int] = None
|
||||||
stagger_index: Optional[int] = None
|
stagger_index: Optional[int] = None
|
||||||
background_color: Optional[Color] = None
|
background_color: Optional[str] = None
|
||||||
|
|
||||||
properties: Optional[Properties] = None
|
properties: Optional[Properties] = None
|
||||||
|
|
||||||
|
|
||||||
'''
|
"""
|
||||||
[22:16] <__m4ch1n3__> i would "[i for i in int_list if i < littler_then_value]"
|
[22:16] <__m4ch1n3__> i would "[i for i in int_list if i < littler_then_value]"
|
||||||
[22:16] <__m4ch1n3__> it returns a list of integers below "littler_then_value"
|
[22:16] <__m4ch1n3__> it returns a list of integers below "littler_then_value"
|
||||||
[22:17] <__m4ch1n3__> !py3 [i for i in [1,2,3,4,1,2,3,4] if i < 3]
|
[22:17] <__m4ch1n3__> !py3 [i for i in [1,2,3,4,1,2,3,4] if i < 3]
|
||||||
@@ -606,6 +599,4 @@ class TileMap:
|
|||||||
[22:23] <codebot> __m4ch1n3__: 100
|
[22:23] <codebot> __m4ch1n3__: 100
|
||||||
[22:23] == markb1 [~mbiggers@45.36.35.206] has quit [Ping timeout: 245 seconds]
|
[22:23] == markb1 [~mbiggers@45.36.35.206] has quit [Ping timeout: 245 seconds]
|
||||||
[22:23] <__m4ch1n3__> !py3 max(i for i in [1, 10, 100] if i < 242)
|
[22:23] <__m4ch1n3__> !py3 max(i for i in [1, 10, 100] if i < 242)
|
||||||
'''
|
"""
|
||||||
|
|
||||||
#buffer
|
|
||||||
|
|||||||
@@ -1,581 +0,0 @@
|
|||||||
import functools
|
|
||||||
import re
|
|
||||||
import base64
|
|
||||||
import zlib
|
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from typing import *
|
|
||||||
|
|
||||||
import pytiled_parser.objects as objects
|
|
||||||
import pytiled_parser.utilities as utilities
|
|
||||||
|
|
||||||
import xml.etree.ElementTree as etree
|
|
||||||
|
|
||||||
def _decode_base64_data(data_text, compression, layer_width):
|
|
||||||
tile_grid: List[List[int]] = [[]]
|
|
||||||
|
|
||||||
unencoded_data = base64.b64decode(data_text)
|
|
||||||
if compression == "zlib":
|
|
||||||
unzipped_data = zlib.decompress(unencoded_data)
|
|
||||||
elif compression == "gzip":
|
|
||||||
unzipped_data = gzip.decompress(unencoded_data)
|
|
||||||
elif compression is None:
|
|
||||||
unzipped_data = unencoded_data
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Unsupported compression type '{compression}'.")
|
|
||||||
|
|
||||||
# Turn bytes into 4-byte integers
|
|
||||||
byte_count = 0
|
|
||||||
int_count = 0
|
|
||||||
int_value = 0
|
|
||||||
row_count = 0
|
|
||||||
for byte in unzipped_data:
|
|
||||||
int_value += byte << (byte_count * 8)
|
|
||||||
byte_count += 1
|
|
||||||
if byte_count % 4 == 0:
|
|
||||||
byte_count = 0
|
|
||||||
int_count += 1
|
|
||||||
tile_grid[row_count].append(int_value)
|
|
||||||
int_value = 0
|
|
||||||
if int_count % layer_width == 0:
|
|
||||||
row_count += 1
|
|
||||||
tile_grid.append([])
|
|
||||||
|
|
||||||
tile_grid.pop()
|
|
||||||
return tile_grid
|
|
||||||
|
|
||||||
|
|
||||||
def _decode_csv_layer(data_text):
|
|
||||||
"""
|
|
||||||
Decodes csv encoded layer data.
|
|
||||||
|
|
||||||
Credit:
|
|
||||||
"""
|
|
||||||
tile_grid = []
|
|
||||||
lines = data_text.split("\n")
|
|
||||||
# remove erronious empty lists due to a newline being on both ends of text
|
|
||||||
lines = lines[1:]
|
|
||||||
lines = lines[:-1]
|
|
||||||
for line in lines:
|
|
||||||
line_list = line.split(",")
|
|
||||||
while '' in line_list:
|
|
||||||
line_list.remove('')
|
|
||||||
line_list_int = [int(item) for item in line_list]
|
|
||||||
tile_grid.append(line_list_int)
|
|
||||||
|
|
||||||
return tile_grid
|
|
||||||
|
|
||||||
|
|
||||||
def _decode_data(element: etree.Element, layer_width: int, encoding: str,
|
|
||||||
compression: Optional[str]) -> List[List[int]]:
|
|
||||||
"""
|
|
||||||
Decodes data or chunk data.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
:element (Element): Element to have text decoded.
|
|
||||||
:layer_width (int): Number of tiles per column in this layer. Used
|
|
||||||
for determining when to cut off a row when decoding base64
|
|
||||||
encoding layers.
|
|
||||||
:encoding (str): Encoding format of the layer data.
|
|
||||||
:compression (str): Compression format of the layer data.
|
|
||||||
"""
|
|
||||||
# etree.Element.text comes with an appended and a prepended '\n'
|
|
||||||
supported_encodings = ['base64', 'csv']
|
|
||||||
if encoding not in supported_encodings:
|
|
||||||
raise ValueError('{encoding} is not a valid encoding')
|
|
||||||
|
|
||||||
supported_compression = [None, 'gzip', 'zlib']
|
|
||||||
if compression is not None:
|
|
||||||
if encoding != 'base64':
|
|
||||||
raise ValueError('{encoding} does not support compression')
|
|
||||||
if compression not in supported_compression:
|
|
||||||
raise ValueError('{compression} is not a valid compression type')
|
|
||||||
|
|
||||||
try:
|
|
||||||
data_text = element.text # type: ignore
|
|
||||||
except AttributeError:
|
|
||||||
raise AttributeError('{element} lacks layer data.')
|
|
||||||
|
|
||||||
if encoding == 'csv':
|
|
||||||
return _decode_csv_layer(data_text)
|
|
||||||
|
|
||||||
return _decode_base64_data(data_text, compression, layer_width)
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_data(element: etree.Element,
|
|
||||||
layer_width: int) -> objects.LayerData:
|
|
||||||
"""
|
|
||||||
Parses layer data.
|
|
||||||
|
|
||||||
Will parse CSV, base64, gzip-base64, or zlip-base64 encoded data.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
:element (Element): Data element to parse.
|
|
||||||
:width (int): Layer width. Used for base64 decoding.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
:LayerData: Data object containing layer data or chunks of data.
|
|
||||||
"""
|
|
||||||
encoding = element.attrib['encoding']
|
|
||||||
compression = None
|
|
||||||
try:
|
|
||||||
compression = element.attrib['compression']
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
chunk_elements = element.findall('./chunk')
|
|
||||||
if chunk_elements:
|
|
||||||
chunks: List[objects.Chunk] = []
|
|
||||||
for chunk_element in chunk_elements:
|
|
||||||
x = int(chunk_element.attrib['x'])
|
|
||||||
y = int(chunk_element.attrib['y'])
|
|
||||||
location = objects.OrderedPair(x, y)
|
|
||||||
width = int(chunk_element.attrib['width'])
|
|
||||||
height = int(chunk_element.attrib['height'])
|
|
||||||
layer_data = _decode_data(chunk_element, layer_width, encoding,
|
|
||||||
compression)
|
|
||||||
chunks.append(objects.Chunk(location, width, height, layer_data))
|
|
||||||
return chunks
|
|
||||||
|
|
||||||
return _decode_data(element, layer_width, encoding, compression)
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_layer(element: etree.Element,
|
|
||||||
layer_type: objects.LayerType) -> objects.Layer:
|
|
||||||
"""
|
|
||||||
Parse layer element given.
|
|
||||||
"""
|
|
||||||
width = int(element.attrib['width'])
|
|
||||||
height = int(element.attrib['height'])
|
|
||||||
size = objects.OrderedPair(width, height)
|
|
||||||
data_element = element.find('./data')
|
|
||||||
if data_element is not None:
|
|
||||||
data: objects.LayerData = _parse_data(data_element, width)
|
|
||||||
else:
|
|
||||||
raise ValueError('{element} has no child data element.')
|
|
||||||
|
|
||||||
return objects.Layer(size, data, **layer_type.__dict__)
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_layer_type(layer_element: etree.Element) -> objects.LayerType:
|
|
||||||
"""
|
|
||||||
Parse layer type element given.
|
|
||||||
"""
|
|
||||||
id = int(layer_element.attrib['id'])
|
|
||||||
|
|
||||||
name = layer_element.attrib['name']
|
|
||||||
|
|
||||||
layer_type_object = objects.LayerType(id, name)
|
|
||||||
|
|
||||||
try:
|
|
||||||
offset_x = float(layer_element.attrib['offsetx'])
|
|
||||||
except KeyError:
|
|
||||||
offset_x = 0
|
|
||||||
|
|
||||||
try:
|
|
||||||
offset_y = float(layer_element.attrib['offsety'])
|
|
||||||
except KeyError:
|
|
||||||
offset_y = 0
|
|
||||||
offset = objects.OrderedPair(offset_x, offset_y)
|
|
||||||
|
|
||||||
try:
|
|
||||||
layer_type_object.opacity = round(
|
|
||||||
float(layer_element.attrib['opacity']) * 255)
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
properties_element = layer_element.find('./properties')
|
|
||||||
if properties_element is not None:
|
|
||||||
layer_type_object.properties = _parse_properties_element(
|
|
||||||
properties_element)
|
|
||||||
|
|
||||||
if layer_element.tag == 'layer':
|
|
||||||
return _parse_layer(layer_element, layer_type_object)
|
|
||||||
elif layer_element.tag == 'objectgroup':
|
|
||||||
return _parse_object_group(layer_element, layer_type_object)
|
|
||||||
# else:
|
|
||||||
# return _parse_layer_group(layer_element, layer_type_object)
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_object_group(element: etree.Element,
|
|
||||||
layer_type: objects.LayerType) -> objects.ObjectGroup:
|
|
||||||
"""
|
|
||||||
Parse object group element given.
|
|
||||||
"""
|
|
||||||
object_elements = element.findall('./object')
|
|
||||||
tile_objects: List[objects.Object] = []
|
|
||||||
|
|
||||||
for object_element in object_elements:
|
|
||||||
id = int(object_element.attrib['id'])
|
|
||||||
location_x = float(object_element.attrib['x'])
|
|
||||||
location_y = float(object_element.attrib['y'])
|
|
||||||
location = objects.OrderedPair(location_x, location_y)
|
|
||||||
|
|
||||||
object = objects.Object(id, location)
|
|
||||||
|
|
||||||
try:
|
|
||||||
width = float(object_element.attrib['width'])
|
|
||||||
except KeyError:
|
|
||||||
width = 0
|
|
||||||
|
|
||||||
try:
|
|
||||||
height = float(object_element.attrib['height'])
|
|
||||||
except KeyError:
|
|
||||||
height = 0
|
|
||||||
|
|
||||||
object.size = objects.OrderedPair(width, height)
|
|
||||||
|
|
||||||
try:
|
|
||||||
object.opacity = round(
|
|
||||||
float(object_element.attrib['opacity']) * 255)
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
try:
|
|
||||||
object.rotation = int(object_element.attrib['rotation'])
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
try:
|
|
||||||
object.name = object_element.attrib['name']
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
properties_element = object_element.find('./properties')
|
|
||||||
if properties_element is not None:
|
|
||||||
object.properties = _parse_properties_element(properties_element)
|
|
||||||
|
|
||||||
tile_objects.append(object)
|
|
||||||
|
|
||||||
object_group = objects.ObjectGroup(tile_objects, **layer_type.__dict__)
|
|
||||||
try:
|
|
||||||
color = utilities.parse_color(element.attrib['color'])
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
try:
|
|
||||||
draw_order = element.attrib['draworder']
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return object_group
|
|
||||||
|
|
||||||
|
|
||||||
@functools.lru_cache()
|
|
||||||
def _parse_external_tile_set(parent_dir: Path, tile_set_element: etree.Element
|
|
||||||
) -> objects.TileSet:
|
|
||||||
"""
|
|
||||||
Parses an external tile set.
|
|
||||||
|
|
||||||
Caches the results to speed up subsequent instances.
|
|
||||||
"""
|
|
||||||
source = Path(tile_set_element.attrib['source'])
|
|
||||||
tile_set_tree = etree.parse(str(parent_dir / Path(source))).getroot()
|
|
||||||
|
|
||||||
return _parse_tile_set(tile_set_tree)
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_tiles(tile_element_list: List[etree.Element]
|
|
||||||
) -> Dict[int, objects.Tile]:
|
|
||||||
tiles: Dict[int, objects.Tile] = {}
|
|
||||||
for tile_element in tile_element_list:
|
|
||||||
# id is not optional
|
|
||||||
id = int(tile_element.attrib['id'])
|
|
||||||
|
|
||||||
# optional attributes
|
|
||||||
type = None
|
|
||||||
try:
|
|
||||||
type = tile_element.attrib['type']
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
tile_terrain = None
|
|
||||||
try:
|
|
||||||
tile_terrain_attrib = tile_element.attrib['terrain']
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
# an attempt to explain how terrains are handled is below.
|
|
||||||
# 'terrain' attribute is a comma seperated list of 4 values,
|
|
||||||
# each is either an integer or blank
|
|
||||||
|
|
||||||
# convert to list of values
|
|
||||||
terrain_list_attrib = re.split(',', tile_terrain_attrib)
|
|
||||||
# terrain_list is list of indexes of Tileset.terrain_types
|
|
||||||
terrain_list: List[Optional[int]] = []
|
|
||||||
# each index in terrain_list_attrib reffers to a corner
|
|
||||||
for corner in terrain_list_attrib:
|
|
||||||
if corner == '':
|
|
||||||
terrain_list.append(None)
|
|
||||||
else:
|
|
||||||
terrain_list.append(int(corner))
|
|
||||||
tile_terrain = objects.TileTerrain(*terrain_list)
|
|
||||||
|
|
||||||
# tile element optional sub-elements
|
|
||||||
animation: Optional[List[objects.Frame]] = None
|
|
||||||
tile_animation_element = tile_element.find('./animation')
|
|
||||||
if tile_animation_element:
|
|
||||||
animation = []
|
|
||||||
frames = tile_animation_element.findall('./frame')
|
|
||||||
for frame in frames:
|
|
||||||
# tileid reffers to the Tile.id of the animation frame
|
|
||||||
tile_id = int(frame.attrib['tileid'])
|
|
||||||
# duration is in MS. Should perhaps be converted to seconds.
|
|
||||||
# FIXME: make decision
|
|
||||||
duration = int(frame.attrib['duration'])
|
|
||||||
animation.append(objects.Frame(tile_id, duration))
|
|
||||||
|
|
||||||
# if this is None, then the Tile is part of a spritesheet
|
|
||||||
tile_image = None
|
|
||||||
tile_image_element = tile_element.find('./image')
|
|
||||||
if tile_image_element is not None:
|
|
||||||
tile_image = _parse_image_element(tile_image_element)
|
|
||||||
|
|
||||||
object_group = None
|
|
||||||
tile_object_group_element = tile_element.find('./objectgroup')
|
|
||||||
if tile_object_group_element:
|
|
||||||
### FIXME: why did they do this :(
|
|
||||||
pass
|
|
||||||
|
|
||||||
tiles[id] = objects.Tile(id, type, tile_terrain, animation,
|
|
||||||
tile_image, object_group)
|
|
||||||
|
|
||||||
return tiles
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_image_element(image_element: etree.Element) -> objects.Image:
|
|
||||||
"""
|
|
||||||
Parse image element given.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
:Color: Color in Arcade's preffered format.
|
|
||||||
"""
|
|
||||||
source = image_element.attrib['source']
|
|
||||||
|
|
||||||
trans = None
|
|
||||||
try:
|
|
||||||
trans = utilities.parse_color(image_element.attrib['trans'])
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
width = int(image_element.attrib['width'])
|
|
||||||
height = int(image_element.attrib['height'])
|
|
||||||
size = objects.OrderedPair(width, height)
|
|
||||||
|
|
||||||
return objects.Image(source, size, trans)
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_properties_element(properties_element: etree.Element
|
|
||||||
) -> objects.Properties:
|
|
||||||
"""
|
|
||||||
Adds Tiled property to Properties dict.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
:name (str): Name of property.
|
|
||||||
:property_type (str): Type of property. Can be string, int, float,
|
|
||||||
bool, color or file. Defaults to string.
|
|
||||||
:value (str): The value of the property.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
:Properties: Properties Dict object.
|
|
||||||
"""
|
|
||||||
properties: objects.Properties = {}
|
|
||||||
for property_element in properties_element.findall('./property'):
|
|
||||||
name = property_element.attrib['name']
|
|
||||||
try:
|
|
||||||
property_type = property_element.attrib['type']
|
|
||||||
except KeyError:
|
|
||||||
# strings do not have an attribute in property elements
|
|
||||||
property_type = 'string'
|
|
||||||
value = property_element.attrib['value']
|
|
||||||
|
|
||||||
property_types = ['string', 'int', 'float', 'bool', 'color', 'file']
|
|
||||||
assert property_type in property_types, (
|
|
||||||
f"Invalid type for property {name}")
|
|
||||||
|
|
||||||
if property_type == 'int':
|
|
||||||
properties[name] = int(value)
|
|
||||||
elif property_type == 'float':
|
|
||||||
properties[name] = float(value)
|
|
||||||
elif property_type == 'color':
|
|
||||||
properties[name] = utilities.parse_color(value)
|
|
||||||
elif property_type == 'file':
|
|
||||||
properties[name] = Path(value)
|
|
||||||
elif property_type == 'bool':
|
|
||||||
if value == 'true':
|
|
||||||
properties[name] = True
|
|
||||||
else:
|
|
||||||
properties[name] = False
|
|
||||||
else:
|
|
||||||
properties[name] = value
|
|
||||||
|
|
||||||
return properties
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_tile_set(tile_set_element: etree.Element) -> objects.TileSet:
|
|
||||||
"""
|
|
||||||
Parses a tile set that is embedded into a TMX.
|
|
||||||
"""
|
|
||||||
# get all basic attributes
|
|
||||||
name = tile_set_element.attrib['name']
|
|
||||||
max_tile_width = int(tile_set_element.attrib['tilewidth'])
|
|
||||||
max_tile_height = int(tile_set_element.attrib['tileheight'])
|
|
||||||
max_tile_size = objects.OrderedPair(max_tile_width, max_tile_height)
|
|
||||||
|
|
||||||
spacing = None
|
|
||||||
try:
|
|
||||||
spacing = int(tile_set_element.attrib['spacing'])
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
margin = None
|
|
||||||
try:
|
|
||||||
margin = int(tile_set_element.attrib['margin'])
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
tile_count = None
|
|
||||||
try:
|
|
||||||
tile_count = int(tile_set_element.attrib['tilecount'])
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
columns = None
|
|
||||||
try:
|
|
||||||
columns = int(tile_set_element.attrib['columns'])
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
tile_offset = None
|
|
||||||
tileoffset_element = tile_set_element.find('./tileoffset')
|
|
||||||
if tileoffset_element is not None:
|
|
||||||
tile_offset_x = int(tileoffset_element.attrib['x'])
|
|
||||||
tile_offset_y = int(tileoffset_element.attrib['y'])
|
|
||||||
tile_offset = objects.OrderedPair(tile_offset_x, tile_offset_y)
|
|
||||||
|
|
||||||
grid = None
|
|
||||||
grid_element = tile_set_element.find('./grid')
|
|
||||||
if grid_element is not None:
|
|
||||||
grid_orientation = grid_element.attrib['orientation']
|
|
||||||
grid_width = int(grid_element.attrib['width'])
|
|
||||||
grid_height = int(grid_element.attrib['height'])
|
|
||||||
grid = objects.Grid(grid_orientation, grid_width, grid_height)
|
|
||||||
|
|
||||||
properties = None
|
|
||||||
properties_element = tile_set_element.find('./properties')
|
|
||||||
if properties_element is not None:
|
|
||||||
properties = _parse_properties_element(properties_element)
|
|
||||||
|
|
||||||
terrain_types: Optional[List[objects.Terrain]] = None
|
|
||||||
terrain_types_element = tile_set_element.find('./terraintypes')
|
|
||||||
if terrain_types_element is not None:
|
|
||||||
terrain_types = []
|
|
||||||
for terrain in terrain_types_element.findall('./terrain'):
|
|
||||||
name = terrain.attrib['name']
|
|
||||||
terrain_tile = int(terrain.attrib['tile'])
|
|
||||||
terrain_types.append(objects.Terrain(name, terrain_tile))
|
|
||||||
|
|
||||||
image = None
|
|
||||||
image_element = tile_set_element.find('./image')
|
|
||||||
if image_element is not None:
|
|
||||||
image = _parse_image_element(image_element)
|
|
||||||
|
|
||||||
tile_element_list = tile_set_element.findall('./tile')
|
|
||||||
tiles = _parse_tiles(tile_element_list)
|
|
||||||
|
|
||||||
return objects.TileSet(name, max_tile_size, spacing, margin, tile_count,
|
|
||||||
columns, tile_offset, grid, properties, image,
|
|
||||||
terrain_types, tiles)
|
|
||||||
|
|
||||||
|
|
||||||
def parse_tile_map(tmx_file: Union[str, Path]):
|
|
||||||
# setting up XML parsing
|
|
||||||
map_tree = etree.parse(str(tmx_file))
|
|
||||||
map_element = map_tree.getroot()
|
|
||||||
|
|
||||||
# positional arguments for TileMap
|
|
||||||
parent_dir = Path(tmx_file).parent
|
|
||||||
|
|
||||||
version = map_element.attrib['version']
|
|
||||||
tiled_version = map_element.attrib['tiledversion']
|
|
||||||
orientation = map_element.attrib['orientation']
|
|
||||||
render_order = map_element.attrib['renderorder']
|
|
||||||
map_width = int(map_element.attrib['width'])
|
|
||||||
map_height = int(map_element.attrib['height'])
|
|
||||||
map_size = objects.OrderedPair(map_width, map_height)
|
|
||||||
tile_width = int(map_element.attrib['tilewidth'])
|
|
||||||
tile_height = int(map_element.attrib['tileheight'])
|
|
||||||
tile_size = objects.OrderedPair(tile_width, tile_height)
|
|
||||||
|
|
||||||
infinite_attribute = map_element.attrib['infinite']
|
|
||||||
infinite = True if infinite_attribute == 'true' else False
|
|
||||||
|
|
||||||
next_layer_id = int(map_element.attrib['nextlayerid'])
|
|
||||||
next_object_id = int(map_element.attrib['nextobjectid'])
|
|
||||||
|
|
||||||
# parse all tilesets
|
|
||||||
tile_sets: Dict[int, objects.TileSet] = {}
|
|
||||||
tile_set_element_list = map_element.findall('./tileset')
|
|
||||||
for tile_set_element in tile_set_element_list:
|
|
||||||
# tiled docs are ambiguous about the 'firstgid' attribute
|
|
||||||
# current understanding is for the purposes of mapping the layer
|
|
||||||
# data to the tile set data, add the 'firstgid' value to each
|
|
||||||
# tile 'id'; this means that the 'firstgid' is specific to each,
|
|
||||||
# tile set as they pertain to the map, not tile set specific as
|
|
||||||
# the tiled docs can make it seem
|
|
||||||
# 'firstgid' is saved beside each TileMap
|
|
||||||
firstgid = int(tile_set_element.attrib['firstgid'])
|
|
||||||
try:
|
|
||||||
# check if is an external TSX
|
|
||||||
source = tile_set_element.attrib['source']
|
|
||||||
except KeyError:
|
|
||||||
# the tile set in embedded
|
|
||||||
name = tile_set_element.attrib['name']
|
|
||||||
tile_sets[firstgid] = _parse_tile_set(tile_set_element)
|
|
||||||
else:
|
|
||||||
# tile set is external
|
|
||||||
tile_sets[firstgid] = _parse_external_tile_set(
|
|
||||||
parent_dir, tile_set_element)
|
|
||||||
|
|
||||||
# parse all layers
|
|
||||||
layers: List[objects.LayerType] = []
|
|
||||||
layer_tags = ['layer', 'objectgroup', 'group']
|
|
||||||
for element in map_element.findall('./'):
|
|
||||||
if element.tag not in layer_tags:
|
|
||||||
# only layer_tags are layer elements
|
|
||||||
continue
|
|
||||||
layers.append(_parse_layer_type(element))
|
|
||||||
|
|
||||||
tile_map = objects.TileMap(parent_dir, version, tiled_version,
|
|
||||||
orientation, render_order, map_size, tile_size,
|
|
||||||
infinite, next_layer_id, next_object_id,
|
|
||||||
tile_sets, layers)
|
|
||||||
|
|
||||||
try:
|
|
||||||
tile_map.hex_side_length = int(map_element.attrib['hexsidelength'])
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
try:
|
|
||||||
tile_map.stagger_axis = int(map_element.attrib['staggeraxis'])
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
try:
|
|
||||||
tile_map.stagger_index = int(map_element.attrib['staggerindex'])
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
try:
|
|
||||||
backgroundcolor = map_element.attrib['backgroundcolor']
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
tile_map.background_color = utilities.parse_color(backgroundcolor)
|
|
||||||
|
|
||||||
properties_element = map_tree.find('./properties')
|
|
||||||
if properties_element is not None:
|
|
||||||
tile_map.properties = _parse_properties_element(properties_element)
|
|
||||||
|
|
||||||
return tile_map
|
|
||||||
@@ -9,7 +9,7 @@ def parse_color(color: str) -> objects.Color:
|
|||||||
:Color: Color object in the format that Arcade understands.
|
:Color: Color object in the format that Arcade understands.
|
||||||
"""
|
"""
|
||||||
# strip initial '#' character
|
# strip initial '#' character
|
||||||
if not len(color) % 2 == 0: # pylint: disable=C2001
|
if not len(color) % 2 == 0:
|
||||||
color = color[1:]
|
color = color[1:]
|
||||||
|
|
||||||
if len(color) == 6:
|
if len(color) == 6:
|
||||||
@@ -25,3 +25,21 @@ def parse_color(color: str) -> objects.Color:
|
|||||||
blue = int(color[6:8], 16)
|
blue = int(color[6:8], 16)
|
||||||
|
|
||||||
return objects.Color(red, green, blue, alpha)
|
return objects.Color(red, green, blue, alpha)
|
||||||
|
|
||||||
|
|
||||||
|
def get_tile_by_gid(tile_sets: objects.TileSetDict, gid: int) -> objects.Tile:
|
||||||
|
"""Gets Tile from a global tile ID.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tile_sets (objects.TileSetDict): TileSetDict from TileMap.
|
||||||
|
gid (int): Global tile ID of the tile to be returned.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
objects.Tile: The Tile object reffered to by the global tile ID.
|
||||||
|
"""
|
||||||
|
for tileset_key, tileset in tile_sets.items():
|
||||||
|
for tile_key, tile in tileset.tiles.items():
|
||||||
|
tile_gid = tile.id + tileset_key
|
||||||
|
if tile_gid == gid:
|
||||||
|
return tile
|
||||||
|
return None
|
||||||
|
|||||||
742
pytiled_parser/xml_parser.py
Normal file
742
pytiled_parser/xml_parser.py
Normal file
@@ -0,0 +1,742 @@
|
|||||||
|
import functools
|
||||||
|
import base64
|
||||||
|
import gzip
|
||||||
|
import re
|
||||||
|
import zlib
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from typing import Callable, Dict, List, Optional, Tuple, Union
|
||||||
|
import xml.etree.ElementTree as etree
|
||||||
|
|
||||||
|
import pytiled_parser.objects as objects
|
||||||
|
import pytiled_parser.utilities as utilities
|
||||||
|
|
||||||
|
|
||||||
|
def _decode_base64_data(
|
||||||
|
data_text: str, layer_width: int, compression: Optional[str] = None
|
||||||
|
) -> List[List[int]]:
|
||||||
|
tile_grid: List[List[int]] = [[]]
|
||||||
|
|
||||||
|
unencoded_data = base64.b64decode(data_text)
|
||||||
|
if compression == "zlib":
|
||||||
|
unzipped_data = zlib.decompress(unencoded_data)
|
||||||
|
elif compression == "gzip":
|
||||||
|
unzipped_data = gzip.decompress(unencoded_data)
|
||||||
|
elif compression is None:
|
||||||
|
unzipped_data = unencoded_data
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unsupported compression type '{compression}'.")
|
||||||
|
|
||||||
|
# Turn bytes into 4-byte integers
|
||||||
|
byte_count = 0
|
||||||
|
int_count = 0
|
||||||
|
int_value = 0
|
||||||
|
row_count = 0
|
||||||
|
for byte in unzipped_data:
|
||||||
|
int_value += byte << (byte_count * 8)
|
||||||
|
byte_count += 1
|
||||||
|
if byte_count % 4 == 0:
|
||||||
|
byte_count = 0
|
||||||
|
int_count += 1
|
||||||
|
tile_grid[row_count].append(int_value)
|
||||||
|
int_value = 0
|
||||||
|
if int_count % layer_width == 0:
|
||||||
|
row_count += 1
|
||||||
|
tile_grid.append([])
|
||||||
|
|
||||||
|
tile_grid.pop()
|
||||||
|
return tile_grid
|
||||||
|
|
||||||
|
|
||||||
|
def _decode_csv_data(data_text: str) -> List[List[int]]:
|
||||||
|
"""Decodes csv encoded layer data.
|
||||||
|
|
||||||
|
Credit:
|
||||||
|
"""
|
||||||
|
tile_grid = []
|
||||||
|
lines: List[str] = data_text.split("\n")
|
||||||
|
# remove erronious empty lists due to a newline being on both ends of text
|
||||||
|
lines = lines[1:-1]
|
||||||
|
for line in lines:
|
||||||
|
line_list = line.split(",")
|
||||||
|
# FIXME: what is this for?
|
||||||
|
while "" in line_list:
|
||||||
|
line_list.remove("")
|
||||||
|
line_list_int = [int(item) for item in line_list]
|
||||||
|
tile_grid.append(line_list_int)
|
||||||
|
|
||||||
|
return tile_grid
|
||||||
|
|
||||||
|
|
||||||
|
def _decode_data(
|
||||||
|
element: etree.Element,
|
||||||
|
layer_width: int,
|
||||||
|
encoding: str,
|
||||||
|
compression: Optional[str],
|
||||||
|
) -> List[List[int]]:
|
||||||
|
"""Decodes data or chunk data.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
:element (Element): Element to have text decoded.
|
||||||
|
:layer_width (int): Number of tiles per column in this layer. Used
|
||||||
|
for determining when to cut off a row when decoding base64
|
||||||
|
encoding layers.
|
||||||
|
:encoding (str): Encoding format of the layer data.
|
||||||
|
:compression (str): Compression format of the layer data.
|
||||||
|
"""
|
||||||
|
# etree.Element.text comes with an appended and a prepended '\n'
|
||||||
|
supported_encodings = ["base64", "csv"]
|
||||||
|
if encoding not in supported_encodings:
|
||||||
|
raise ValueError("{encoding} is not a valid encoding")
|
||||||
|
|
||||||
|
supported_compression = [None, "gzip", "zlib"]
|
||||||
|
if compression is not None:
|
||||||
|
if encoding != "base64":
|
||||||
|
raise ValueError("{encoding} does not support compression")
|
||||||
|
if compression not in supported_compression:
|
||||||
|
raise ValueError("{compression} is not a valid compression type")
|
||||||
|
|
||||||
|
try:
|
||||||
|
data_text: str = element.text # type: ignore
|
||||||
|
except AttributeError:
|
||||||
|
raise AttributeError(f"{element} lacks layer data.")
|
||||||
|
|
||||||
|
if encoding == "csv":
|
||||||
|
return _decode_csv_data(data_text)
|
||||||
|
|
||||||
|
return _decode_base64_data(data_text, compression, layer_width)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_data(
|
||||||
|
element: etree.Element, layer_width: int
|
||||||
|
) -> objects.LayerData:
|
||||||
|
"""Parses layer data.
|
||||||
|
|
||||||
|
Will parse CSV, base64, gzip-base64, or zlip-base64 encoded data.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
:element (Element): Data element to parse.
|
||||||
|
:width (int): Layer width. Used for base64 decoding.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
:LayerData: Data object containing layer data or chunks of data.
|
||||||
|
"""
|
||||||
|
encoding = element.attrib["encoding"]
|
||||||
|
compression = None
|
||||||
|
try:
|
||||||
|
compression = element.attrib["compression"]
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
chunk_elements = element.findall("./chunk")
|
||||||
|
if chunk_elements:
|
||||||
|
chunks: List[objects.Chunk] = []
|
||||||
|
for chunk_element in chunk_elements:
|
||||||
|
x = int(chunk_element.attrib["x"])
|
||||||
|
y = int(chunk_element.attrib["y"])
|
||||||
|
location = objects.OrderedPair(x, y)
|
||||||
|
width = int(chunk_element.attrib["width"])
|
||||||
|
height = int(chunk_element.attrib["height"])
|
||||||
|
layer_data = _decode_data(
|
||||||
|
chunk_element, layer_width, encoding, compression
|
||||||
|
)
|
||||||
|
chunks.append(objects.Chunk(location, width, height, layer_data))
|
||||||
|
return chunks
|
||||||
|
|
||||||
|
return _decode_data(element, layer_width, encoding, compression)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_layer(
|
||||||
|
layer_element: etree.Element
|
||||||
|
) -> Tuple[
|
||||||
|
int,
|
||||||
|
str,
|
||||||
|
Optional[objects.OrderedPair],
|
||||||
|
Optional[float],
|
||||||
|
Optional[objects.Properties],
|
||||||
|
]:
|
||||||
|
"""Parses all of the attributes for a Layer object.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
layer_element: The layer element to be parsed.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
FIXME
|
||||||
|
"""
|
||||||
|
id = int(layer_element.attrib["id"])
|
||||||
|
|
||||||
|
name = layer_element.attrib["name"]
|
||||||
|
|
||||||
|
offset: Optional[objects.OrderedPair]
|
||||||
|
offset_x_attrib = layer_element.attrib.get("offsetx")
|
||||||
|
offset_y_attrib = layer_element.attrib.get("offsety")
|
||||||
|
# If any offset is present, we need to return an OrderedPair
|
||||||
|
# Unknown if one of the offsets could be absent.
|
||||||
|
if any([offset_x_attrib, offset_y_attrib]):
|
||||||
|
if offset_x_attrib:
|
||||||
|
offset_x = float(offset_x_attrib)
|
||||||
|
else:
|
||||||
|
offset_x = 0.0
|
||||||
|
if offset_y_attrib:
|
||||||
|
offset_y = float(offset_y_attrib)
|
||||||
|
else:
|
||||||
|
offset_y = 0.0
|
||||||
|
|
||||||
|
offset = objects.OrderedPair(offset_x, offset_y)
|
||||||
|
else:
|
||||||
|
offset = None
|
||||||
|
|
||||||
|
opacity: Optional[float]
|
||||||
|
opacity_attrib = layer_element.attrib.get("opacity")
|
||||||
|
if opacity_attrib:
|
||||||
|
opacity = float(opacity_attrib)
|
||||||
|
else:
|
||||||
|
opacity = None
|
||||||
|
|
||||||
|
properties: Optional[objects.Properties]
|
||||||
|
properties_element = layer_element.find("./properties")
|
||||||
|
if properties_element is not None:
|
||||||
|
properties = _parse_properties_element(properties_element)
|
||||||
|
else:
|
||||||
|
properties = None
|
||||||
|
|
||||||
|
return id, name, offset, opacity, properties
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_tile_layer(element: etree.Element,) -> objects.TileLayer:
|
||||||
|
"""Parses tile layer element.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
element: The layer element to be parsed.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
TileLayer: The tile layer object.
|
||||||
|
"""
|
||||||
|
id, name, offset, opacity, properties = _parse_layer(element)
|
||||||
|
|
||||||
|
width = int(element.attrib["width"])
|
||||||
|
height = int(element.attrib["height"])
|
||||||
|
size = objects.Size(width, height)
|
||||||
|
|
||||||
|
data_element = element.find("./data")
|
||||||
|
if data_element is not None:
|
||||||
|
data: objects.LayerData = _parse_data(data_element, width)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"{element} has no child data element.")
|
||||||
|
|
||||||
|
return objects.TileLayer(
|
||||||
|
id, name, offset, opacity, properties, size, data
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_objects(
|
||||||
|
object_elements: List[etree.Element]
|
||||||
|
) -> List[objects.TiledObject]:
|
||||||
|
"""Parses objects found in the 'objectgroup' element.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
object_elements: List of object elements to be parsed.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: List of parsed tiled objects.
|
||||||
|
"""
|
||||||
|
tiled_objects: List[objects.TiledObject] = []
|
||||||
|
|
||||||
|
for object_element in object_elements:
|
||||||
|
id = int(object_element.attrib["id"])
|
||||||
|
location_x = float(object_element.attrib["x"])
|
||||||
|
location_y = float(object_element.attrib["y"])
|
||||||
|
location = objects.OrderedPair(location_x, location_y)
|
||||||
|
|
||||||
|
tiled_object = objects.TiledObject(id, location)
|
||||||
|
|
||||||
|
try:
|
||||||
|
width = float(object_element.attrib["width"])
|
||||||
|
except KeyError:
|
||||||
|
width = 0
|
||||||
|
|
||||||
|
try:
|
||||||
|
height = float(object_element.attrib["height"])
|
||||||
|
except KeyError:
|
||||||
|
height = 0
|
||||||
|
|
||||||
|
tiled_object.size = objects.Size(width, height)
|
||||||
|
|
||||||
|
try:
|
||||||
|
tiled_object.opacity = float(object_element.attrib["opacity"])
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
tiled_object.rotation = int(object_element.attrib["rotation"])
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
tiled_object.name = object_element.attrib["name"]
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
tiled_object.type = object_element.attrib["type"]
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
properties_element = object_element.find("./properties")
|
||||||
|
if properties_element is not None:
|
||||||
|
tiled_object.properties = _parse_properties_element(
|
||||||
|
properties_element
|
||||||
|
)
|
||||||
|
|
||||||
|
tiled_objects.append(tiled_object)
|
||||||
|
|
||||||
|
return tiled_objects
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_object_layer(element: etree.Element,) -> objects.ObjectLayer:
|
||||||
|
"""Parse the objectgroup element given.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
layer_type (objects.LayerType):
|
||||||
|
id: The id of the layer.
|
||||||
|
name: The name of the layer.
|
||||||
|
offset: The offset of the layer.
|
||||||
|
opacity: The opacity of the layer.
|
||||||
|
properties: The Properties object of the layer.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ObjectLayer: The object layer object.
|
||||||
|
"""
|
||||||
|
id, name, offset, opacity, properties = _parse_layer(element)
|
||||||
|
|
||||||
|
tiled_objects = _parse_objects(element.findall("./object"))
|
||||||
|
|
||||||
|
try:
|
||||||
|
color = element.attrib["color"]
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
draw_order = element.attrib["draworder"]
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return objects.ObjectLayer(
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
offset,
|
||||||
|
opacity,
|
||||||
|
properties,
|
||||||
|
tiled_objects,
|
||||||
|
color,
|
||||||
|
draw_order,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_layer_group(element: etree.Element,) -> objects.LayerGroup:
|
||||||
|
"""Parse the objectgroup element given.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
layer_type (objects.LayerType):
|
||||||
|
id: The id of the layer.
|
||||||
|
name: The name of the layer.
|
||||||
|
offset: The offset of the layer.
|
||||||
|
opacity: The opacity of the layer.
|
||||||
|
properties: The Properties object of the layer.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
LayerGroup: The layer group object.
|
||||||
|
"""
|
||||||
|
id, name, offset, opacity, properties = _parse_layer(element)
|
||||||
|
|
||||||
|
layers = _get_layers(element)
|
||||||
|
|
||||||
|
return objects.LayerGroup(id, name, offset, opacity, properties, layers)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_layer_parser(
|
||||||
|
layer_tag: str
|
||||||
|
) -> Optional[Callable[[etree.Element], objects.Layer]]:
|
||||||
|
"""Gets a the parser for the layer type specified.
|
||||||
|
|
||||||
|
Layer tags are 'layer' for a tile layer, 'objectgroup' for an object
|
||||||
|
layer, and 'group' for a layer group. If anything else is passed,
|
||||||
|
returns None.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
layer_tag: Specifies the layer type to be parsed based on the element
|
||||||
|
tag.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Callable: the function to be used to parse the layer.
|
||||||
|
None: The element is not a map layer.
|
||||||
|
"""
|
||||||
|
if layer_tag == "layer":
|
||||||
|
return _parse_tile_layer
|
||||||
|
elif layer_tag == "objectgroup":
|
||||||
|
return _parse_object_layer
|
||||||
|
elif layer_tag == "group":
|
||||||
|
return _parse_layer_group
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _get_layers(map_element: etree.Element) -> List[objects.Layer]:
|
||||||
|
"""Parse layer type element given.
|
||||||
|
|
||||||
|
Retains draw order based on the returned lists index FIXME: confirm
|
||||||
|
|
||||||
|
Args:
|
||||||
|
map_element: The element containing the layer.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[Layer]: A list of the layers, ordered by draw order.
|
||||||
|
FIXME: confirm
|
||||||
|
"""
|
||||||
|
layers: List[objects.Layer] = []
|
||||||
|
for element in map_element.findall("./"):
|
||||||
|
layer_parser = _get_layer_parser(element.tag)
|
||||||
|
if layer_parser:
|
||||||
|
layers.append(layer_parser(element))
|
||||||
|
|
||||||
|
return layers
|
||||||
|
|
||||||
|
|
||||||
|
@functools.lru_cache()
|
||||||
|
def _parse_external_tile_set(
|
||||||
|
parent_dir: Path, tile_set_element: etree.Element
|
||||||
|
) -> objects.TileSet:
|
||||||
|
"""Parses an external tile set.
|
||||||
|
|
||||||
|
Caches the results to speed up subsequent maps with identical tilesets.
|
||||||
|
"""
|
||||||
|
source = Path(tile_set_element.attrib["source"])
|
||||||
|
tile_set_tree = etree.parse(str(parent_dir / Path(source))).getroot()
|
||||||
|
|
||||||
|
return _parse_tile_set(tile_set_tree)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_hitboxes(element: etree.Element) -> List[objects.TiledObject]:
|
||||||
|
"""Parses all hitboxes for a given tile."""
|
||||||
|
return _parse_objects(element.findall("./object"))
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_tiles(
|
||||||
|
tile_element_list: List[etree.Element]
|
||||||
|
) -> Dict[int, objects.Tile]:
|
||||||
|
tiles: Dict[int, objects.Tile] = {}
|
||||||
|
for tile_element in tile_element_list:
|
||||||
|
# id is not optional
|
||||||
|
id = int(tile_element.attrib["id"])
|
||||||
|
|
||||||
|
# optional attributes
|
||||||
|
tile_type = None
|
||||||
|
try:
|
||||||
|
tile_type = tile_element.attrib["type"]
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
tile_terrain = None
|
||||||
|
try:
|
||||||
|
tile_terrain_attrib = tile_element.attrib["terrain"]
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
# below is an attempt to explain how terrains are handled.
|
||||||
|
#'terrain' attribute is a comma seperated list of 4 values,
|
||||||
|
# each is either an integer or blank
|
||||||
|
|
||||||
|
# convert to list of values
|
||||||
|
terrain_list_attrib = re.split(",", tile_terrain_attrib)
|
||||||
|
# terrain_list is list of indexes of Tileset.terrain_types
|
||||||
|
terrain_list: List[Optional[int]] = []
|
||||||
|
# each index in terrain_list_attrib reffers to a corner
|
||||||
|
for corner in terrain_list_attrib:
|
||||||
|
if corner == "":
|
||||||
|
terrain_list.append(None)
|
||||||
|
else:
|
||||||
|
terrain_list.append(int(corner))
|
||||||
|
tile_terrain = objects.TileTerrain(*terrain_list)
|
||||||
|
|
||||||
|
# tile element optional sub-elements
|
||||||
|
animation: Optional[List[objects.Frame]] = None
|
||||||
|
tile_animation_element = tile_element.find("./animation")
|
||||||
|
if tile_animation_element:
|
||||||
|
animation = []
|
||||||
|
frames = tile_animation_element.findall("./frame")
|
||||||
|
for frame in frames:
|
||||||
|
# tileid reffers to the Tile.id of the animation frame
|
||||||
|
tile_id = int(frame.attrib["tileid"])
|
||||||
|
# duration is in MS. Should perhaps be converted to seconds.
|
||||||
|
# FIXME: make decision
|
||||||
|
duration = int(frame.attrib["duration"])
|
||||||
|
animation.append(objects.Frame(tile_id, duration))
|
||||||
|
|
||||||
|
# if this is None, then the Tile is part of a spritesheet
|
||||||
|
tile_image = None
|
||||||
|
tile_image_element = tile_element.find("./image")
|
||||||
|
if tile_image_element is not None:
|
||||||
|
tile_image = _parse_image_element(tile_image_element)
|
||||||
|
|
||||||
|
hitboxes = None
|
||||||
|
tile_hitboxes_element = tile_element.find("./objectgroup")
|
||||||
|
if tile_hitboxes_element is not None:
|
||||||
|
hitboxes = _parse_hitboxes(tile_hitboxes_element)
|
||||||
|
|
||||||
|
tiles[id] = objects.Tile(
|
||||||
|
id, tile_type, tile_terrain, animation, tile_image, hitboxes
|
||||||
|
)
|
||||||
|
|
||||||
|
return tiles
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_image_element(image_element: etree.Element) -> objects.Image:
|
||||||
|
"""Parse image element given.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
: Color in Arcade's preffered format.
|
||||||
|
"""
|
||||||
|
image = objects.Image(image_element.attrib["source"])
|
||||||
|
|
||||||
|
width_attrib = image_element.attrib.get("width")
|
||||||
|
height_attrib = image_element.attrib.get("height")
|
||||||
|
|
||||||
|
if width_attrib and height_attrib:
|
||||||
|
image.size = objects.Size(int(width_attrib), int(height_attrib))
|
||||||
|
|
||||||
|
try:
|
||||||
|
image.trans = image_element.attrib["trans"]
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return image
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_properties_element(
|
||||||
|
properties_element: etree.Element
|
||||||
|
) -> objects.Properties:
|
||||||
|
"""Adds Tiled property to Properties dict.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
:name (str): Name of property.
|
||||||
|
:property_type (str): Type of property. Can be string, int, float,
|
||||||
|
bool, color or file. Defaults to string.
|
||||||
|
:value (str): The value of the property.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
:Properties: Properties Dict object.
|
||||||
|
"""
|
||||||
|
properties: objects.Properties = {}
|
||||||
|
for property_element in properties_element.findall("./property"):
|
||||||
|
name = property_element.attrib["name"]
|
||||||
|
try:
|
||||||
|
property_type = property_element.attrib["type"]
|
||||||
|
except KeyError:
|
||||||
|
# strings do not have an attribute in property elements
|
||||||
|
property_type = "string"
|
||||||
|
value = property_element.attrib["value"]
|
||||||
|
|
||||||
|
property_types = ["string", "int", "float", "bool", "color", "file"]
|
||||||
|
assert (
|
||||||
|
property_type in property_types
|
||||||
|
), f"Invalid type for property {name}"
|
||||||
|
|
||||||
|
if property_type == "int":
|
||||||
|
properties[name] = int(value)
|
||||||
|
elif property_type == "float":
|
||||||
|
properties[name] = float(value)
|
||||||
|
elif property_type == "color":
|
||||||
|
properties[name] = value
|
||||||
|
elif property_type == "file":
|
||||||
|
properties[name] = Path(value)
|
||||||
|
elif property_type == "bool":
|
||||||
|
if value == "true":
|
||||||
|
properties[name] = True
|
||||||
|
else:
|
||||||
|
properties[name] = False
|
||||||
|
else:
|
||||||
|
properties[name] = value
|
||||||
|
|
||||||
|
return properties
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_tile_set(tile_set_element: etree.Element) -> objects.TileSet:
|
||||||
|
"""
|
||||||
|
Parses a tile set that is embedded into a TMX.
|
||||||
|
"""
|
||||||
|
# get all basic attributes
|
||||||
|
name = tile_set_element.attrib["name"]
|
||||||
|
max_tile_width = int(tile_set_element.attrib["tilewidth"])
|
||||||
|
max_tile_height = int(tile_set_element.attrib["tileheight"])
|
||||||
|
max_tile_size = objects.Size(max_tile_width, max_tile_height)
|
||||||
|
|
||||||
|
spacing = None
|
||||||
|
try:
|
||||||
|
spacing = int(tile_set_element.attrib["spacing"])
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
margin = None
|
||||||
|
try:
|
||||||
|
margin = int(tile_set_element.attrib["margin"])
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
tile_count = None
|
||||||
|
try:
|
||||||
|
tile_count = int(tile_set_element.attrib["tilecount"])
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
columns = None
|
||||||
|
try:
|
||||||
|
columns = int(tile_set_element.attrib["columns"])
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
tile_offset = None
|
||||||
|
tileoffset_element = tile_set_element.find("./tileoffset")
|
||||||
|
if tileoffset_element is not None:
|
||||||
|
tile_offset_x = int(tileoffset_element.attrib["x"])
|
||||||
|
tile_offset_y = int(tileoffset_element.attrib["y"])
|
||||||
|
tile_offset = objects.OrderedPair(tile_offset_x, tile_offset_y)
|
||||||
|
|
||||||
|
grid = None
|
||||||
|
grid_element = tile_set_element.find("./grid")
|
||||||
|
if grid_element is not None:
|
||||||
|
grid_orientation = grid_element.attrib["orientation"]
|
||||||
|
grid_width = int(grid_element.attrib["width"])
|
||||||
|
grid_height = int(grid_element.attrib["height"])
|
||||||
|
grid = objects.Grid(grid_orientation, grid_width, grid_height)
|
||||||
|
|
||||||
|
properties = None
|
||||||
|
properties_element = tile_set_element.find("./properties")
|
||||||
|
if properties_element is not None:
|
||||||
|
properties = _parse_properties_element(properties_element)
|
||||||
|
|
||||||
|
terrain_types: Optional[List[objects.Terrain]] = None
|
||||||
|
terrain_types_element = tile_set_element.find("./terraintypes")
|
||||||
|
if terrain_types_element is not None:
|
||||||
|
terrain_types = []
|
||||||
|
for terrain in terrain_types_element.findall("./terrain"):
|
||||||
|
name = terrain.attrib["name"]
|
||||||
|
terrain_tile = int(terrain.attrib["tile"])
|
||||||
|
terrain_types.append(objects.Terrain(name, terrain_tile))
|
||||||
|
|
||||||
|
image = None
|
||||||
|
image_element = tile_set_element.find("./image")
|
||||||
|
if image_element is not None:
|
||||||
|
image = _parse_image_element(image_element)
|
||||||
|
|
||||||
|
tile_element_list = tile_set_element.findall("./tile")
|
||||||
|
tiles = _parse_tiles(tile_element_list)
|
||||||
|
|
||||||
|
return objects.TileSet(
|
||||||
|
name,
|
||||||
|
max_tile_size,
|
||||||
|
spacing,
|
||||||
|
margin,
|
||||||
|
tile_count,
|
||||||
|
columns,
|
||||||
|
tile_offset,
|
||||||
|
grid,
|
||||||
|
properties,
|
||||||
|
image,
|
||||||
|
terrain_types,
|
||||||
|
tiles,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_tile_map(tmx_file: Union[str, Path]) -> objects.TileMap:
|
||||||
|
# setting up XML parsing
|
||||||
|
map_tree = etree.parse(str(tmx_file))
|
||||||
|
map_element = map_tree.getroot()
|
||||||
|
|
||||||
|
# positional arguments for TileMap
|
||||||
|
parent_dir = Path(tmx_file).parent
|
||||||
|
|
||||||
|
version = map_element.attrib["version"]
|
||||||
|
tiled_version = map_element.attrib["tiledversion"]
|
||||||
|
orientation = map_element.attrib["orientation"]
|
||||||
|
render_order = map_element.attrib["renderorder"]
|
||||||
|
map_width = int(map_element.attrib["width"])
|
||||||
|
map_height = int(map_element.attrib["height"])
|
||||||
|
map_size = objects.Size(map_width, map_height)
|
||||||
|
tile_width = int(map_element.attrib["tilewidth"])
|
||||||
|
tile_height = int(map_element.attrib["tileheight"])
|
||||||
|
tile_size = objects.Size(tile_width, tile_height)
|
||||||
|
|
||||||
|
infinite_attribute = map_element.attrib["infinite"]
|
||||||
|
infinite = True if infinite_attribute == "true" else False
|
||||||
|
|
||||||
|
next_layer_id = int(map_element.attrib["nextlayerid"])
|
||||||
|
next_object_id = int(map_element.attrib["nextobjectid"])
|
||||||
|
|
||||||
|
# parse all tilesets
|
||||||
|
tile_sets: Dict[int, objects.TileSet] = {}
|
||||||
|
tile_set_element_list = map_element.findall("./tileset")
|
||||||
|
for tile_set_element in tile_set_element_list:
|
||||||
|
# tiled docs are ambiguous about the 'firstgid' attribute
|
||||||
|
# current understanding is for the purposes of mapping the layer
|
||||||
|
# data to the tile set data, add the 'firstgid' value to each
|
||||||
|
# tile 'id'; this means that the 'firstgid' is specific to each,
|
||||||
|
# tile set as they pertain to the map, not tile set specific as
|
||||||
|
# the tiled docs can make it seem
|
||||||
|
# 'firstgid' is saved beside each TileMap
|
||||||
|
firstgid = int(tile_set_element.attrib["firstgid"])
|
||||||
|
try:
|
||||||
|
# check if is an external TSX
|
||||||
|
source = tile_set_element.attrib["source"]
|
||||||
|
except KeyError:
|
||||||
|
# the tile set in embedded
|
||||||
|
name = tile_set_element.attrib["name"]
|
||||||
|
tile_sets[firstgid] = _parse_tile_set(tile_set_element)
|
||||||
|
else:
|
||||||
|
# tile set is external
|
||||||
|
tile_sets[firstgid] = _parse_external_tile_set(
|
||||||
|
parent_dir, tile_set_element
|
||||||
|
)
|
||||||
|
|
||||||
|
layers = _get_layers(map_element)
|
||||||
|
|
||||||
|
tile_map = objects.TileMap(
|
||||||
|
parent_dir,
|
||||||
|
version,
|
||||||
|
tiled_version,
|
||||||
|
orientation,
|
||||||
|
render_order,
|
||||||
|
map_size,
|
||||||
|
tile_size,
|
||||||
|
infinite,
|
||||||
|
next_layer_id,
|
||||||
|
next_object_id,
|
||||||
|
tile_sets,
|
||||||
|
layers,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
tile_map.hex_side_length = int(map_element.attrib["hexsidelength"])
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
tile_map.stagger_axis = int(map_element.attrib["staggeraxis"])
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
tile_map.stagger_index = int(map_element.attrib["staggerindex"])
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
tile_map.background_color = map_element.attrib["backgroundcolor"]
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
properties_element = map_tree.find("./properties")
|
||||||
|
if properties_element is not None:
|
||||||
|
tile_map.properties = _parse_properties_element(properties_element)
|
||||||
|
|
||||||
|
return tile_map
|
||||||
58
setup.py
58
setup.py
@@ -3,38 +3,36 @@ import sys
|
|||||||
from setuptools import setup
|
from setuptools import setup
|
||||||
|
|
||||||
BUILD = 0
|
BUILD = 0
|
||||||
VERSION = '0.1'
|
VERSION = "0.0.1"
|
||||||
RELEASE = VERSION
|
RELEASE = VERSION
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
readme = path.join(path.dirname(path.abspath(__file__)), 'README.md')
|
readme = path.join(path.dirname(path.abspath(__file__)), "README.md")
|
||||||
with open(readme, 'r') as f:
|
with open(readme, "r") as f:
|
||||||
long_desc = f.read()
|
long_desc = f.read()
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='pytiled_parser',
|
name="pytiled_parser",
|
||||||
version=RELEASE,
|
version=RELEASE,
|
||||||
description='Python Library for parsing Tiled Map Editor maps.',
|
description="Python Library for parsing Tiled Map Editor maps.",
|
||||||
long_description=long_desc,
|
long_description=long_desc,
|
||||||
author='Benjamin Kirkbride',
|
author="Benjamin Kirkbride",
|
||||||
author_email='BenjaminKirkbride@gmail.com',
|
author_email="BenjaminKirkbride@gmail.com",
|
||||||
license='MIT',
|
license="MIT",
|
||||||
url='https://github.com/Beefy-Swain/pytiled_parser',
|
url="https://github.com/Beefy-Swain/pytiled_parser",
|
||||||
download_url='https://github.com/Beefy-Swain/pytiled_parser',
|
download_url="https://github.com/Beefy-Swain/pytiled_parser",
|
||||||
install_requires=[
|
install_requires=["dataclasses"],
|
||||||
'dataclasses',
|
packages=["pytiled_parser"],
|
||||||
],
|
classifiers=[
|
||||||
packages=['pytiled_parser'],
|
"Development Status :: 1 - Planning",
|
||||||
classifiers=[
|
"Intended Audience :: Developers",
|
||||||
'Development Status :: 1 - Planning',
|
"License :: OSI Approved :: MIT License",
|
||||||
'Intended Audience :: Developers',
|
"Operating System :: OS Independent",
|
||||||
'License :: OSI Approved :: MIT License',
|
"Programming Language :: Python",
|
||||||
'Operating System :: OS Independent',
|
"Programming Language :: Python :: 3.6",
|
||||||
'Programming Language :: Python',
|
"Programming Language :: Python :: 3.7",
|
||||||
'Programming Language :: Python :: 3.6',
|
"Programming Language :: Python :: Implementation :: CPython",
|
||||||
'Programming Language :: Python :: 3.7',
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
||||||
'Programming Language :: Python :: Implementation :: CPython',
|
],
|
||||||
'Topic :: Software Development :: Libraries :: Python Modules',
|
test_suite="tests",
|
||||||
],
|
)
|
||||||
test_suite='tests',
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -1,25 +1,20 @@
|
|||||||
{ 'background_color': None,
|
{ 'background_color': None,
|
||||||
'height': 6,
|
|
||||||
'hex_side_length': None,
|
'hex_side_length': None,
|
||||||
'infinite': False,
|
'infinite': False,
|
||||||
'layers': [ Layer(width=8, height=6, data=[[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]], id=1, name='Tile Layer 1', offset=OrderedPair(x=0, y=0), opacity=255, properties=None)],
|
'layers': [ TileLayer(id=1, name='Tile Layer 1', offset=None, opacity=None, properties=None, size=Size(width=10, height=10), data=[[1, 2, 3, 4, 5, 6, 7, 8, 30, 30], [9, 10, 11, 12, 13, 14, 15, 16, 30, 30], [17, 18, 19, 20, 21, 22, 23, 24, 30, 30], [25, 26, 27, 28, 29, 30, 31, 32, 30, 30], [33, 34, 35, 36, 37, 38, 39, 40, 30, 30], [41, 42, 43, 44, 45, 46, 47, 48, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30]]),
|
||||||
'next_layer_id': 2,
|
TileLayer(id=2, name='Tile Layer 2', offset=None, opacity=0.5, properties=None, size=Size(width=10, height=10), data=[[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 46, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 6, 7, 7, 7, 7, 7, 8, 0], [0, 0, 14, 15, 15, 15, 15, 15, 16, 0], [0, 0, 22, 23, 23, 23, 23, 23, 24, 0]]),
|
||||||
'next_object_id': 1,
|
LayerGroup(id=3, name='Group 1', offset=None, opacity=None, properties={'bool property': True}, layers=[TileLayer(id=5, name='Tile Layer 4', offset=OrderedPair(x=49.0, y=-50.0), opacity=None, properties=None, size=Size(width=10, height=10), data=[[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 31, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]), TileLayer(id=4, name='Tile Layer 3', offset=None, opacity=None, properties=None, size=Size(width=10, height=10), data=[[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [1, 2, 3, 0, 0, 0, 0, 0, 0, 0], [9, 10, 11, 0, 0, 0, 0, 0, 0, 0], [17, 18, 19, 0, 0, 0, 0, 0, 0, 0]])]),
|
||||||
|
ObjectLayer(id=6, name='Object Layer 1', offset=OrderedPair(x=4.66667, y=-4.33333), opacity=0.9, properties=None, tiled_objects=[TiledObject(id=1, location=OrderedPair(x=200.25, y=210.75), size=Size(width=47.25, height=25.0), rotation=15, opacity=1, name='rectangle 1', type='rectangle type', properties=None, template=None), TiledObject(id=2, location=OrderedPair(x=252.5, y=87.75), size=Size(width=0, height=0), rotation=-21, opacity=1, name='polygon 1', type='polygon type', properties=None, template=None), TiledObject(id=3, location=OrderedPair(x=198.75, y=102.5), size=Size(width=17.75, height=14.25), rotation=0, opacity=1, name='elipse 1', type='elipse type', properties=None, template=None), TiledObject(id=4, location=OrderedPair(x=174.25, y=186.0), size=Size(width=0, height=0), rotation=0, opacity=1, name='point 1', type='point type', properties=None, template=None), TiledObject(id=7, location=OrderedPair(x=11.3958, y=48.5833), size=Size(width=107.625, height=27.25), rotation=0, opacity=1, name='insert text 1', type='insert text type', properties=None, template=None), TiledObject(id=6, location=OrderedPair(x=47.25, y=72.5), size=Size(width=47.0, height=53.0), rotation=31, opacity=1, name='inserted tile 1', type='inserted tile type', properties={'tile property bool': True}, template=None), TiledObject(id=8, location=OrderedPair(x=144.667, y=112.0), size=Size(width=0, height=0), rotation=0, opacity=1, name='polyline 1', type='polyline type', properties=None, template=None), TiledObject(id=9, location=OrderedPair(x=69.8333, y=168.333), size=Size(width=0, height=0), rotation=0, opacity=1, name='polygon 2', type='polygon type', properties=None, template=None)], color=Color(red=0, green=0, blue=0, alpha=255), draw_order='index')],
|
||||||
|
'map_size': Size(width=10, height=10),
|
||||||
|
'next_layer_id': 16,
|
||||||
|
'next_object_id': 10,
|
||||||
'orientation': 'orthogonal',
|
'orientation': 'orthogonal',
|
||||||
'parent_dir': PosixPath('/home/ben/Projects/pytiled_parser/pytiled_parser-venv/pytiled_parser/tests/test_data'),
|
'parent_dir': PosixPath('/home/benk/Projects/pytiled_parser/venv/pytiled_parser/tests/test_data'),
|
||||||
'properties': { 'bool property - false': False,
|
'properties': None,
|
||||||
'bool property - true': True,
|
|
||||||
'color property': Color(red=73, green=252, blue=255, alpha=255),
|
|
||||||
'file property': PosixPath('/var/log/syslog'),
|
|
||||||
'float property': 1.23456789,
|
|
||||||
'int property': 13,
|
|
||||||
'string property': 'Hello, World!!'},
|
|
||||||
'render_order': 'right-down',
|
'render_order': 'right-down',
|
||||||
'stagger_axis': None,
|
'stagger_axis': None,
|
||||||
'stagger_index': None,
|
'stagger_index': None,
|
||||||
'tile_height': 32,
|
'tile_sets': { 1: TileSet(name='tile_set_image', max_tile_size=Size(width=32, height=32), spacing=1, margin=1, tile_count=48, columns=8, tile_offset=None, grid=None, properties=None, image=Image(source='images/tmw_desert_spacing.png', size=Size(width=265, height=199), trans=None), terrain_types=None, tiles={})},
|
||||||
'tile_sets': { 1: TileSet(name='tile_set_image', max_tile_size=OrderedPair(x=32, y=32), spacing=1, margin=1, tile_count=48, columns=8, tile_offset=None, grid=None, properties=None, image=Image(source='images/tmw_desert_spacing.png', trans=None, width=265, height=199), terrain_types=None, tiles={})},
|
'tile_size': Size(width=32, height=32),
|
||||||
'tile_width': 32,
|
|
||||||
'tiled_version': '1.2.3',
|
'tiled_version': '1.2.3',
|
||||||
'version': '1.2',
|
'version': '1.2'}
|
||||||
'width': 8}
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ pp = pprint.PrettyPrinter(indent=4, compact=True, width=100)
|
|||||||
|
|
||||||
pp = pp.pprint
|
pp = pp.pprint
|
||||||
|
|
||||||
MAP_NAME = '/home/ben/Projects/pytiled_parser/pytiled_parser-venv/pytiled_parser/tests/test_data/test_map_simple.tmx'
|
MAP_NAME = "/home/benk/Projects/pytiled_parser/venv/pytiled_parser/tests/test_data/test_map_image_tile_set.tmx"
|
||||||
|
|
||||||
map = pytiled_parser.parse_tile_map(MAP_NAME)
|
map = pytiled_parser.parse_tile_map(MAP_NAME)
|
||||||
|
|
||||||
|
|||||||
23
tests/test_data/test_map_simple_hitboxes.tmx
Normal file
23
tests/test_data/test_map_simple_hitboxes.tmx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<map version="1.2" tiledversion="1.2.3" orientation="orthogonal" renderorder="right-down" width="8" height="6" tilewidth="32" tileheight="32" infinite="0" nextlayerid="2" nextobjectid="1">
|
||||||
|
<properties>
|
||||||
|
<property name="bool property - false" type="bool" value="false"/>
|
||||||
|
<property name="bool property - true" type="bool" value="true"/>
|
||||||
|
<property name="color property" type="color" value="#ff49fcff"/>
|
||||||
|
<property name="file property" type="file" value="../../../../../../../../var/log/syslog"/>
|
||||||
|
<property name="float property" type="float" value="1.23456789"/>
|
||||||
|
<property name="int property" type="int" value="13"/>
|
||||||
|
<property name="string property" value="Hello, World!!"/>
|
||||||
|
</properties>
|
||||||
|
<tileset firstgid="1" source="tile_set_image_hitboxes.tsx"/>
|
||||||
|
<layer id="1" name="Tile Layer 1" width="8" height="6">
|
||||||
|
<data encoding="csv">
|
||||||
|
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
|
||||||
|
</data>
|
||||||
|
</layer>
|
||||||
|
</map>
|
||||||
18
tests/test_data/test_map_simple_meme.tmx
Normal file
18
tests/test_data/test_map_simple_meme.tmx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<map version="1.2" tiledversion="1.2.3" orientation="orthogonal" renderorder="right-down" width="8" height="6" tilewidth="32" tileheight="32" infinite="0" nextlayerid="2" nextobjectid="1">
|
||||||
|
<properties>
|
||||||
|
<property name="bool property - false" type="bool" value="false"/>
|
||||||
|
<property name="bool property - true" type="bool" value="true"/>
|
||||||
|
<property name="color property" type="color" value="#ff49fcff"/>
|
||||||
|
<property name="file property" type="file" value="../../../../../../../../var/log/syslog"/>
|
||||||
|
<property name="float property" type="float" value="1.23456789"/>
|
||||||
|
<property name="int property" type="int" value="13"/>
|
||||||
|
<property name="string property" value="Hello, World!!"/>
|
||||||
|
</properties>
|
||||||
|
<tileset firstgid="1" source="tile_set_image.tsx"/>
|
||||||
|
<layer id="1" name="Tile Layer 1" width="8" height="6">
|
||||||
|
<data encoding="base64" compression="gzip">
|
||||||
|
H4sIAAAAAAAAAw3DBRKCQAAAwDMRA7BQLMTE9v+vY3dmWyGEth279uwbOTB26MixExNTM6fOnLtwae7KtYUbt+7ce7D0aOXJsxev3rxb+/Dpy7cfv/782wAcvDirwAAAAA==
|
||||||
|
</data>
|
||||||
|
</layer>
|
||||||
|
</map>
|
||||||
40
tests/test_data/tile_set_image_hitboxes.tsx
Normal file
40
tests/test_data/tile_set_image_hitboxes.tsx
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<tileset version="1.2" tiledversion="1.2.3" name="tile_set_image" tilewidth="32" tileheight="32" spacing="1" margin="1" tilecount="48" columns="8">
|
||||||
|
<image source="images/tmw_desert_spacing.png" width="265" height="199"/>
|
||||||
|
<tile id="9">
|
||||||
|
<objectgroup draworder="index">
|
||||||
|
<object id="2" name="wall" type="rectangle type" x="1" y="1" width="32" height="32" rotation="1"/>
|
||||||
|
</objectgroup>
|
||||||
|
</tile>
|
||||||
|
<tile id="19">
|
||||||
|
<objectgroup draworder="index">
|
||||||
|
<object id="1" name="wall corner" type="polygon type" x="32" y="1" rotation="1">
|
||||||
|
<polygon points="0,0 -32,0 -32,32 -16,32.1818 -15.8182,16.9091 0.181818,17.0909"/>
|
||||||
|
</object>
|
||||||
|
</objectgroup>
|
||||||
|
</tile>
|
||||||
|
<tile id="20">
|
||||||
|
<objectgroup draworder="index">
|
||||||
|
<object id="1" name="polyline" type="polyline type" x="1.45455" y="1.45455" rotation="1">
|
||||||
|
<polyline points="0,0 25.0909,21.2727 9.63636,28.3636"/>
|
||||||
|
</object>
|
||||||
|
</objectgroup>
|
||||||
|
</tile>
|
||||||
|
<tile id="31">
|
||||||
|
<objectgroup draworder="index">
|
||||||
|
<object id="1" name="rock 1" type="elipse type" x="5.09091" y="2.54545" width="19.6364" height="19.2727" rotation="1">
|
||||||
|
<ellipse/>
|
||||||
|
</object>
|
||||||
|
<object id="2" name="rock 2" type="elipse type" x="16.1818" y="22" width="8.54545" height="8.36364" rotation="-1">
|
||||||
|
<ellipse/>
|
||||||
|
</object>
|
||||||
|
</objectgroup>
|
||||||
|
</tile>
|
||||||
|
<tile id="45">
|
||||||
|
<objectgroup draworder="index">
|
||||||
|
<object id="1" name="sign" type="point type" x="14.7273" y="26.3636">
|
||||||
|
<point/>
|
||||||
|
</object>
|
||||||
|
</objectgroup>
|
||||||
|
</tile>
|
||||||
|
</tileset>
|
||||||
170
tests/unit2/test_parser.py
Normal file
170
tests/unit2/test_parser.py
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
import xml.etree.ElementTree as etree
|
||||||
|
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from typing import Callable, List, Optional, Tuple
|
||||||
|
|
||||||
|
from pytiled_parser import objects, xml_parser, utilities
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def does_not_raise():
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
def _get_root_element(xml: str) -> etree.Element:
|
||||||
|
return etree.fromstring(xml)
|
||||||
|
|
||||||
|
|
||||||
|
layer_data = [
|
||||||
|
(
|
||||||
|
'<layer id="1" name="Tile Layer 1" width="10" height="10">'
|
||||||
|
"</layer>",
|
||||||
|
(int(1), "Tile Layer 1", None, None, None),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'<layer id="2" name="Tile Layer 2" width="10" height="10" opacity="0.5">'
|
||||||
|
"</layer>",
|
||||||
|
(int(2), "Tile Layer 2", None, float(0.5), None),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'<layer id="5" name="Tile Layer 4" width="10" height="10" offsetx="49" offsety="-50">'
|
||||||
|
"<properties>"
|
||||||
|
"</properties>"
|
||||||
|
"</layer>",
|
||||||
|
(
|
||||||
|
int(5),
|
||||||
|
"Tile Layer 4",
|
||||||
|
objects.OrderedPair(49, -50),
|
||||||
|
None,
|
||||||
|
"properties",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("xml,expected", layer_data)
|
||||||
|
def test_parse_layer(xml, expected, monkeypatch):
|
||||||
|
def mockreturn(properties):
|
||||||
|
return "properties"
|
||||||
|
|
||||||
|
monkeypatch.setattr(xml_parser, "_parse_properties_element", mockreturn)
|
||||||
|
|
||||||
|
assert xml_parser._parse_layer(_get_root_element(xml)) == expected
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"test_input,expected",
|
||||||
|
[
|
||||||
|
("#001122", (0x00, 0x11, 0x22, 0xFF)),
|
||||||
|
("001122", (0x00, 0x11, 0x22, 0xFF)),
|
||||||
|
("#FF001122", (0x00, 0x11, 0x22, 0xFF)),
|
||||||
|
("FF001122", (0x00, 0x11, 0x22, 0xFF)),
|
||||||
|
("FF001122", (0x00, 0x11, 0x22, 0xFF)),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_color_parsing(test_input, expected):
|
||||||
|
"""
|
||||||
|
Tiled has a few different types of color representations.
|
||||||
|
"""
|
||||||
|
assert utilities.parse_color(test_input) == expected
|
||||||
|
|
||||||
|
|
||||||
|
data_csv = [
|
||||||
|
(
|
||||||
|
"\n1,2,3,4,5,6,7,8,\n"
|
||||||
|
"9,10,11,12,13,14,15,16,\n"
|
||||||
|
"17,18,19,20,21,22,23,24,\n"
|
||||||
|
"25,26,27,28,29,30,31,32,\n"
|
||||||
|
"33,34,35,36,37,38,39,40,\n"
|
||||||
|
"41,42,43,44,45,46,47,48\n",
|
||||||
|
[
|
||||||
|
[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],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
("\n0,0,0,0,0\n", [[0, 0, 0, 0, 0]]),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("data_csv,expected", data_csv)
|
||||||
|
def test_decode_csv_data(data_csv, expected):
|
||||||
|
assert xml_parser._decode_csv_data(data_csv) == expected
|
||||||
|
|
||||||
|
|
||||||
|
data_base64 = [
|
||||||
|
(
|
||||||
|
"AQAAAAIAAAADAAAABAAAAAUAAAAGAAAABwAAAAgAAAAJAAAACgAAAAsAAAAMAAAADQAAAA4AAAAPAAAAEAAAABEAAAASAAAAEwAAABQAAAAVAAAAFgAAABcAAAAYAAAAGQAAABoAAAAbAAAAHAAAAB0AAAAeAAAAHwAAACAAAAAhAAAAIgAAACMAAAAkAAAAJQAAACYAAAAnAAAAKAAAACkAAAAqAAAAKwAAACwAAAAtAAAALgAAAC8AAAAwAAAA",
|
||||||
|
8,
|
||||||
|
None,
|
||||||
|
[
|
||||||
|
[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],
|
||||||
|
],
|
||||||
|
does_not_raise(),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"eJwNwwUSgkAAAMAzEQOwUCzExPb/r2N3ZlshhLYdu/bsGzkwdujIsRMTUzOnzpy7cGnuyrWFG7fu3Huw9GjlybMXr968W/vw6cu3H7/+/NsAMw8EmQ==",
|
||||||
|
8,
|
||||||
|
"zlib",
|
||||||
|
[
|
||||||
|
[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],
|
||||||
|
],
|
||||||
|
does_not_raise(),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"H4sIAAAAAAAAAw3DBRKCQAAAwDMRA7BQLMTE9v+vY3dmWyGEth279uwbOTB26MixExNTM6fOnLtwae7KtYUbt+7ce7D0aOXJsxev3rxb+/Dpy7cfv/782wAcvDirwAAAAA==",
|
||||||
|
8,
|
||||||
|
"gzip",
|
||||||
|
[
|
||||||
|
[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],
|
||||||
|
],
|
||||||
|
does_not_raise(),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"SGVsbG8gV29ybGQh",
|
||||||
|
8,
|
||||||
|
"lzma",
|
||||||
|
[
|
||||||
|
[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],
|
||||||
|
],
|
||||||
|
pytest.raises(ValueError),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"data_base64,width,compression,expected,raises", data_base64
|
||||||
|
)
|
||||||
|
def test_decode_base64_data(
|
||||||
|
data_base64, width, compression, expected, raises
|
||||||
|
):
|
||||||
|
with raises:
|
||||||
|
assert (
|
||||||
|
xml_parser._decode_base64_data(data_base64, width, compression)
|
||||||
|
== expected
|
||||||
|
)
|
||||||
@@ -15,7 +15,9 @@ def test_map_simple():
|
|||||||
"""
|
"""
|
||||||
TMX with a very simple spritesheet tile set and some properties.
|
TMX with a very simple spritesheet tile set and some properties.
|
||||||
"""
|
"""
|
||||||
map = pytiled_parser.parse_tile_map(Path("../test_data/test_map_simple.tmx"))
|
map = pytiled_parser.parse_tile_map(
|
||||||
|
Path("../test_data/test_map_simple.tmx")
|
||||||
|
)
|
||||||
|
|
||||||
# map
|
# map
|
||||||
# unsure how to get paths to compare propperly
|
# unsure how to get paths to compare propperly
|
||||||
@@ -37,13 +39,13 @@ def test_map_simple():
|
|||||||
assert map.background_color == None
|
assert map.background_color == None
|
||||||
|
|
||||||
assert map.properties == {
|
assert map.properties == {
|
||||||
"bool property - false": False,
|
"bool property - false": False,
|
||||||
"bool property - true": True,
|
"bool property - true": True,
|
||||||
"color property": (0x49, 0xfc, 0xff, 0xff),
|
"color property": (0x49, 0xFC, 0xFF, 0xFF),
|
||||||
"file property": Path("/var/log/syslog"),
|
"file property": Path("/var/log/syslog"),
|
||||||
"float property": 1.23456789,
|
"float property": 1.23456789,
|
||||||
"int property": 13,
|
"int property": 13,
|
||||||
"string property": "Hello, World!!"
|
"string property": "Hello, World!!",
|
||||||
}
|
}
|
||||||
|
|
||||||
# tileset
|
# tileset
|
||||||
@@ -59,7 +61,8 @@ def test_map_simple():
|
|||||||
|
|
||||||
# unsure how to get paths to compare propperly
|
# unsure how to get paths to compare propperly
|
||||||
assert str(map.tile_sets[1].image.source) == (
|
assert str(map.tile_sets[1].image.source) == (
|
||||||
"images/tmw_desert_spacing.png")
|
"images/tmw_desert_spacing.png"
|
||||||
|
)
|
||||||
assert map.tile_sets[1].image.trans == None
|
assert map.tile_sets[1].image.trans == None
|
||||||
assert map.tile_sets[1].image.size == (265, 199)
|
assert map.tile_sets[1].image.size == (265, 199)
|
||||||
|
|
||||||
@@ -67,28 +70,31 @@ def test_map_simple():
|
|||||||
assert map.tile_sets[1].tiles == {}
|
assert map.tile_sets[1].tiles == {}
|
||||||
|
|
||||||
# layers
|
# layers
|
||||||
assert map.layers[0].data == [[1,2,3,4,5,6,7,8],
|
assert map.layers[0].data == [
|
||||||
[9,10,11,12,13,14,15,16],
|
[1, 2, 3, 4, 5, 6, 7, 8],
|
||||||
[17,18,19,20,21,22,23,24],
|
[9, 10, 11, 12, 13, 14, 15, 16],
|
||||||
[25,26,27,28,29,30,31,32],
|
[17, 18, 19, 20, 21, 22, 23, 24],
|
||||||
[33,34,35,36,37,38,39,40],
|
[25, 26, 27, 28, 29, 30, 31, 32],
|
||||||
[41,42,43,44,45,46,47,48]]
|
[33, 34, 35, 36, 37, 38, 39, 40],
|
||||||
|
[41, 42, 43, 44, 45, 46, 47, 48],
|
||||||
|
]
|
||||||
assert map.layers[0].id == 1
|
assert map.layers[0].id == 1
|
||||||
assert map.layers[0].name == "Tile Layer 1"
|
assert map.layers[0].name == "Tile Layer 1"
|
||||||
assert map.layers[0].offset == (0, 0)
|
assert map.layers[0].offset == None
|
||||||
assert map.layers[0].opacity == 0xFF
|
assert map.layers[0].opacity == None
|
||||||
assert map.layers[0].properties == None
|
assert map.layers[0].properties == None
|
||||||
assert map.layers[0].size == (8, 6)
|
assert map.layers[0].size == (8, 6)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"test_input,expected", [
|
"test_input,expected",
|
||||||
("#001122", (0x00, 0x11, 0x22, 0xff)),
|
[
|
||||||
("001122", (0x00, 0x11, 0x22, 0xff)),
|
("#001122", (0x00, 0x11, 0x22, 0xFF)),
|
||||||
("#FF001122", (0x00, 0x11, 0x22, 0xff)),
|
("001122", (0x00, 0x11, 0x22, 0xFF)),
|
||||||
("FF001122", (0x00, 0x11, 0x22, 0xff)),
|
("#FF001122", (0x00, 0x11, 0x22, 0xFF)),
|
||||||
("FF001122", (0x00, 0x11, 0x22, 0xff)),
|
("FF001122", (0x00, 0x11, 0x22, 0xFF)),
|
||||||
]
|
("FF001122", (0x00, 0x11, 0x22, 0xFF)),
|
||||||
|
],
|
||||||
)
|
)
|
||||||
def test_color_parsing(test_input, expected):
|
def test_color_parsing(test_input, expected):
|
||||||
"""
|
"""
|
||||||
Reference in New Issue
Block a user