mirror of
https://github.com/OMGeeky/pytiled_parser.git
synced 2025-12-26 17:02:28 +01:00
oof
This commit is contained in:
3
.style.yapf
Normal file
3
.style.yapf
Normal file
@@ -0,0 +1,3 @@
|
||||
[style]
|
||||
based_on_style = google
|
||||
column_limit = 78
|
||||
@@ -0,0 +1,4 @@
|
||||
from . import utilities
|
||||
from . import objects
|
||||
|
||||
from .parser import parse_tile_map
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
611
pytiled_parser/objects.py
Normal file
611
pytiled_parser/objects.py
Normal file
@@ -0,0 +1,611 @@
|
||||
"""
|
||||
pytiled_parser objects for Tiled maps.
|
||||
"""
|
||||
|
||||
import dataclasses
|
||||
import functools
|
||||
import re
|
||||
|
||||
from collections import OrderedDict
|
||||
from pathlib import Path
|
||||
|
||||
import xml.etree.ElementTree as etree
|
||||
|
||||
from typing import * # pylint: disable=W0401
|
||||
|
||||
|
||||
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):
|
||||
"""
|
||||
Color object.
|
||||
|
||||
Attributes:
|
||||
:red (int): Red, between 1 and 255.
|
||||
:green (int): Green, between 1 and 255.
|
||||
:blue (int): Blue, between 1 and 255.
|
||||
:alpha (int): Alpha, between 1 and 255.
|
||||
"""
|
||||
red: int
|
||||
green: int
|
||||
blue: int
|
||||
alpha: int
|
||||
|
||||
|
||||
class OrderedPair(NamedTuple):
|
||||
"""
|
||||
OrderedPair NamedTuple.
|
||||
|
||||
Attributes:
|
||||
:x (Union[int, float]): X coordinate.
|
||||
:y (Union[int, float]): Y coordinate.
|
||||
"""
|
||||
x: Union[int, float]
|
||||
y: Union[int, float]
|
||||
|
||||
|
||||
class Template:
|
||||
"""
|
||||
FIXME TODO
|
||||
"""
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class Chunk:
|
||||
"""
|
||||
Chunk object for infinite maps.
|
||||
|
||||
See: https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#chunk
|
||||
|
||||
Attributes:
|
||||
:location (OrderedPair): Location of chunk in tiles.
|
||||
:width (int): The width of the chunk in tiles.
|
||||
:height (int): The height of the chunk in tiles.
|
||||
:layer_data (List[List(int)]): The global tile IDs in chunky
|
||||
according to row.
|
||||
"""
|
||||
location: OrderedPair
|
||||
width: int
|
||||
height: int
|
||||
chunk_data: List[List[int]]
|
||||
|
||||
|
||||
class Image(NamedTuple):
|
||||
"""
|
||||
Image object.
|
||||
|
||||
This module does not support embedded data in image elements.
|
||||
|
||||
Attributes:
|
||||
:source (Optional[str]): The reference to the tileset image file.
|
||||
Not that this is a relative path compared to FIXME
|
||||
:trans (Optional[Color]): Defines a specific color that is treated
|
||||
as transparent.
|
||||
:width (Optional[str]): The image width in pixels
|
||||
(optional, used for tile index correction when the image changes).
|
||||
:height (Optional[str]): The image height in pixels (optional).
|
||||
"""
|
||||
source: str
|
||||
size: OrderedPair
|
||||
trans: Optional[Color]
|
||||
|
||||
|
||||
Properties = Dict[str, Union[int, float, Color, Path, str]]
|
||||
|
||||
|
||||
class Grid(NamedTuple):
|
||||
"""
|
||||
Contains info for isometric maps.
|
||||
|
||||
This element is only used in case of isometric orientation, and
|
||||
determines how tile overlays for terrain and collision information
|
||||
are rendered.
|
||||
"""
|
||||
orientation: str
|
||||
width: int
|
||||
height: int
|
||||
|
||||
|
||||
class Terrain(NamedTuple):
|
||||
"""
|
||||
Terrain object.
|
||||
|
||||
Args:
|
||||
:name (str): The name of the terrain type.
|
||||
:tile (int): The local tile-id of the tile that represents the
|
||||
terrain visually.
|
||||
"""
|
||||
name: str
|
||||
tile: int
|
||||
|
||||
|
||||
class Frame(NamedTuple):
|
||||
"""
|
||||
Animation Frame object.
|
||||
|
||||
This is only used as a part of an animation for Tile objects.
|
||||
|
||||
Args:
|
||||
:tile_id (int): The local ID of a tile within the parent tile set
|
||||
object.
|
||||
:duration (int): How long in milliseconds this frame should be
|
||||
displayed before advancing to the next frame.
|
||||
"""
|
||||
tile_id: int
|
||||
duration: int
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class TileTerrain:
|
||||
"""
|
||||
Defines each corner of a tile by Terrain index in
|
||||
'TileSet.terrain_types'.
|
||||
|
||||
Defaults to 'None'. 'None' means that corner has no terrain.
|
||||
|
||||
Attributes:
|
||||
:top_left (Optional[int]): Top left terrain type.
|
||||
:top_right (Optional[int]): Top right terrain type.
|
||||
:bottom_left (Optional[int]): Bottom left terrain type.
|
||||
:bottom_right (Optional[int]): Bottom right terrain type.
|
||||
"""
|
||||
top_left: Optional[int] = None
|
||||
top_right: Optional[int] = None
|
||||
bottom_left: Optional[int] = None
|
||||
bottom_right: Optional[int] = None
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class _LayerTypeBase:
|
||||
id: int # pylint: disable=C0103
|
||||
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:
|
||||
:layer_element (etree.Element): Element to be parsed into a
|
||||
LayerType object.
|
||||
|
||||
Attributes:
|
||||
:id (int): Unique ID of the layer. Each layer that added to a map
|
||||
gets a unique id. Even if a layer is deleted, no layer ever gets
|
||||
the same ID.
|
||||
:name (Optional[str):] The name of the layer object.
|
||||
:offset (OrderedPair): Rendering offset of the layer object in
|
||||
pixels. (default: (0, 0).
|
||||
:opacity (int): Value between 0 and 255 to determine opacity. NOTE:
|
||||
this value is converted from a float provided by Tiled, so some
|
||||
precision is lost.
|
||||
:properties (Optional[Properties]): Properties object for layer
|
||||
object.
|
||||
"""
|
||||
|
||||
|
||||
LayerData = Union[List[List[int]], List[Chunk]]
|
||||
"""
|
||||
The tile data for one layer.
|
||||
|
||||
Either a 2 dimensional array of integers representing the global tile IDs
|
||||
for a map layer, or a lists of chunks for an infinite map layer.
|
||||
"""
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class _LayerBase:
|
||||
size: OrderedPair
|
||||
data: LayerData
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class Layer(LayerType, _LayerBase):
|
||||
"""
|
||||
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
|
||||
location: OrderedPair
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class _ObjectDefaults:
|
||||
size: OrderedPair = OrderedPair(0, 0)
|
||||
rotation: int = 0
|
||||
opacity: int = 0xFF
|
||||
|
||||
name: Optional[str] = None
|
||||
type: Optional[str] = None
|
||||
|
||||
properties: Optional[Properties] = None
|
||||
template: Optional[Template] = None
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class Object(_ObjectDefaults, _ObjectBase):
|
||||
"""
|
||||
ObjectGroup Object.
|
||||
|
||||
See: \
|
||||
https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#object
|
||||
|
||||
Args:
|
||||
: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
|
||||
gets the same ID.
|
||||
:location (OrderedPair): The location of the object in pixels.
|
||||
:size (OrderedPair): The width of the object in pixels
|
||||
(default: (0, 0)).
|
||||
:rotation (int): The rotation of the object in degrees clockwise
|
||||
(default: 0).
|
||||
:opacity (int): The opacity of the object. (default: 255)
|
||||
:name (Optional[str]): The name of the object.
|
||||
:type (Optional[str]): The type of the object.
|
||||
:properties (Properties): The properties of the Object.
|
||||
:template Optional[Template]: A reference to a Template object
|
||||
FIXME
|
||||
"""
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class RectangleObject(Object):
|
||||
"""
|
||||
Rectangle shape defined by a point, width, and height.
|
||||
|
||||
See: https://doc.mapeditor.org/en/stable/manual/objects/#insert-rectangle
|
||||
(objects in tiled are rectangles by default, so there is no specific
|
||||
documentation on the tmx-map-format page for it.)
|
||||
"""
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class ElipseObject(Object):
|
||||
"""
|
||||
Elipse shape defined by a point, width, and height.
|
||||
|
||||
See: https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#ellipse
|
||||
"""
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class PointObject(Object):
|
||||
"""
|
||||
Point defined by a point (x,y).
|
||||
|
||||
See: https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#point
|
||||
"""
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class _TileObjectBase(_ObjectBase):
|
||||
gid: int
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class TileObject(Object, _TileObjectBase):
|
||||
"""
|
||||
Polygon shape defined by a set of connections between points.
|
||||
|
||||
See: https://doc.mapeditor.org/en/stable/manual/objects/#insert-tile
|
||||
|
||||
Attributes:
|
||||
:gid (int): Refference to a global tile id.
|
||||
"""
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class _PointsObjectBase(_ObjectBase):
|
||||
points: List[OrderedPair]
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class PolygonObject(Object, _PointsObjectBase):
|
||||
"""
|
||||
Polygon shape defined by a set of connections between points.
|
||||
|
||||
See: https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#polygon
|
||||
|
||||
Attributes:
|
||||
:points (List[OrderedPair])
|
||||
"""
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class PolylineObject(Object, _PointsObjectBase):
|
||||
"""
|
||||
Polyline defined by a set of connections between points.
|
||||
|
||||
See: \
|
||||
https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#polyline
|
||||
|
||||
Attributes:
|
||||
:points (List[Tuple[int, int]]): List of coordinates relative to \
|
||||
the location of the object.
|
||||
"""
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class _TextObjectBase(_ObjectBase):
|
||||
text: str
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class _TextObjectDefaults(_ObjectDefaults):
|
||||
font_family: str = 'sans-serif'
|
||||
font_size: int = 16
|
||||
wrap: bool = False
|
||||
color: Color = Color(0xFF, 0, 0, 0)
|
||||
bold: bool = False
|
||||
italic: bool = False
|
||||
underline: bool = False
|
||||
strike_out: bool = False
|
||||
kerning: bool = False
|
||||
horizontal_align: str = 'left'
|
||||
vertical_align: str = 'top'
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class TextObject(Object, _TextObjectDefaults, _TextObjectBase):
|
||||
"""
|
||||
Text object with associated settings.
|
||||
|
||||
See: https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#text
|
||||
and https://doc.mapeditor.org/en/stable/manual/objects/#insert-text
|
||||
|
||||
Attributes:
|
||||
:font_family (str): The font family used (default: “sans-serif”)
|
||||
:font_size (int): The size of the font in pixels. (default: 16)
|
||||
:wrap (bool): Whether word wrapping is enabled. (default: False)
|
||||
:color (Color): Color of the text. (default: #000000)
|
||||
:bold (bool): Whether the font is bold. (default: False)
|
||||
:italic (bool): Whether the font is italic. (default: False)
|
||||
:underline (bool): Whether the text is underlined. (default: False)
|
||||
:strike_out (bool): Whether the text is striked-out. (default: False)
|
||||
:kerning (bool): Whether kerning should be used while rendering the \
|
||||
text. (default: False)
|
||||
:horizontal_align (str): Horizontal alignment of the text \
|
||||
(default: "left")
|
||||
:vertical_align (str): Vertical alignment of the text (defalt: "top")
|
||||
"""
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class _ObjectGroupBase(_LayerTypeBase):
|
||||
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.
|
||||
|
||||
The object group is in fact a map layer, and is hence called \
|
||||
“object layer” in Tiled.
|
||||
|
||||
See: \
|
||||
https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#objectgroup
|
||||
|
||||
Attributes:
|
||||
:color (Optional[Color]): The color used to display the objects
|
||||
in this group. FIXME: editor only?
|
||||
:draworder (str): Whether the objects are drawn according to the
|
||||
order of the 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
|
||||
for more info.
|
||||
:objects (Dict[int, Object]): Dict Object objects by Object.id.
|
||||
"""
|
||||
|
||||
|
||||
class _LayerGroupBase(_LayerTypeBase):
|
||||
layers: Optional[List[LayerType]]
|
||||
|
||||
|
||||
class LayerGroup(LayerType):
|
||||
"""
|
||||
Layer Group.
|
||||
|
||||
A LayerGroup can be thought of as a layer that contains layers
|
||||
(potentially including other LayerGroups).
|
||||
|
||||
Offset and opacity recursively affect child layers.
|
||||
|
||||
See: https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#group
|
||||
|
||||
Attributes:
|
||||
|
||||
"""
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class Tile:
|
||||
"""
|
||||
Individual tile object.
|
||||
|
||||
Args:
|
||||
:id (int): The local tile ID within its tileset.
|
||||
:type (str): The type of the tile. Refers to an object type and is
|
||||
used by tile objects.
|
||||
:terrain (int): Defines the terrain type of each corner of the tile.
|
||||
:animation (List[Frame]): Each tile can have exactly one animation
|
||||
associated with it.
|
||||
"""
|
||||
id: int
|
||||
type: Optional[str]
|
||||
terrain: Optional[TileTerrain]
|
||||
animation: Optional[List[Frame]]
|
||||
image: Optional[Image]
|
||||
hit_box: Optional[List[Object]]
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class TileSet:
|
||||
"""
|
||||
Object for storing a TSX with all associated collision data.
|
||||
|
||||
Args:
|
||||
:name (str): The name of this tileset.
|
||||
:max_tile_size (OrderedPair): The maximum size of a tile in this
|
||||
tile set in pixels.
|
||||
:spacing (int): The spacing in pixels between the tiles in this
|
||||
tileset (applies to the tileset image).
|
||||
:margin (int): The margin around the tiles in this tileset
|
||||
(applies to the tileset image).
|
||||
:tile_count (int): The number of tiles in this tileset.
|
||||
:columns (int): The number of tile columns in the tileset.
|
||||
For image collection tilesets it is editable and is used when
|
||||
displaying the tileset.
|
||||
:grid (Grid): Only used in case of isometric orientation, and
|
||||
determines how tile overlays for terrain and collision information
|
||||
are rendered.
|
||||
:tileoffset (Optional[OrderedPair]): Used to specify an offset in
|
||||
pixels when drawing a tile from the tileset. When not present, no
|
||||
offset is applied.
|
||||
:image (Image): Used for spritesheet tile sets.
|
||||
:terrain_types (Dict[str, int]): List of of terrain types which
|
||||
can be referenced from the terrain attribute of the tile object.
|
||||
Ordered according to the terrain element's appearance in the TSX
|
||||
file.
|
||||
:tiles (Optional[Dict[int, Tile]]): Dict of Tile objects by Tile.id.
|
||||
"""
|
||||
name: str
|
||||
max_tile_size: OrderedPair
|
||||
spacing: Optional[int]
|
||||
margin: Optional[int]
|
||||
tile_count: Optional[int]
|
||||
columns: Optional[int]
|
||||
tile_offset: Optional[OrderedPair]
|
||||
grid: Optional[Grid]
|
||||
properties: Optional[Properties]
|
||||
image: Optional[Image]
|
||||
terrain_types: Optional[List[Terrain]]
|
||||
tiles: Optional[Dict[int, Tile]]
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class TileMap:
|
||||
"""
|
||||
Object for storing a TMX with all associated layers and properties.
|
||||
|
||||
See: https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#map
|
||||
|
||||
Attributes:
|
||||
:parent_dir (Path): The directory the TMX file is in. Used for finding
|
||||
relative paths to TSX files and other assets.
|
||||
:version (str): The TMX format version.
|
||||
:tiledversion (str): The Tiled version used to save the file. May
|
||||
be a date (for snapshot builds).
|
||||
:orientation (str): Map orientation. Tiled supports “orthogonal”,
|
||||
“isometric”, “staggered” and “hexagonal”
|
||||
:renderorder (str): The order in which tiles on tile layers are
|
||||
rendered. Valid values are right-down, right-up, left-down and
|
||||
left-up. In all cases, the map is drawn row-by-row. (only
|
||||
supported for orthogonal maps at the moment)
|
||||
:map_size (OrderedPair): The map width in tiles.
|
||||
:tile_size (OrderedPair): The width of a tile.
|
||||
:infinite (bool): If the map is infinite or not.
|
||||
:hexsidelength (int): Only for hexagonal maps. Determines the width or
|
||||
height (depending on the staggered axis) of the tile’s edge, in
|
||||
pixels.
|
||||
:stagger_axis (str): For staggered and hexagonal maps, determines
|
||||
which axis (“x” or “y”) is staggered.
|
||||
:staggerindex (str): For staggered and hexagonal maps, determines
|
||||
whether the “even” or “odd” indexes along the staggered axis are
|
||||
shifted.
|
||||
:backgroundcolor (##FIXME##): The background color of the map.
|
||||
:nextlayerid (int): Stores the next available ID for new layers.
|
||||
:nextobjectid (int): Stores the next available ID for new objects.
|
||||
:tile_sets (dict[str, TileSet]): Dict of tile sets used
|
||||
in this map. Key is the source for external tile sets or the name
|
||||
for embedded ones. The value is a TileSet object.
|
||||
:layers List[LayerType]: List of layer objects by draw order.
|
||||
"""
|
||||
parent_dir: Path
|
||||
|
||||
version: str
|
||||
tiled_version: str
|
||||
orientation: str
|
||||
render_order: str
|
||||
map_size: OrderedPair
|
||||
tile_size: OrderedPair
|
||||
infinite: bool
|
||||
next_layer_id: int
|
||||
next_object_id: int
|
||||
|
||||
tile_sets: Dict[int, TileSet]
|
||||
layers: List[LayerType]
|
||||
|
||||
hex_side_length: Optional[int] = None
|
||||
stagger_axis: Optional[int] = None
|
||||
stagger_index: Optional[int] = None
|
||||
background_color: Optional[Color] = None
|
||||
|
||||
properties: Optional[Properties] = None
|
||||
|
||||
|
||||
'''
|
||||
[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:17] <__m4ch1n3__> !py3 [i for i in [1,2,3,4,1,2,3,4] if i < 3]
|
||||
[22:17] <codebot> __m4ch1n3__: [1, 2, 1, 2]
|
||||
[22:17] <__m4ch1n3__> !py3 [i for i in [1,2,3,4,1,2,3,4] if i < 4]
|
||||
[22:17] <codebot> __m4ch1n3__: [1, 2, 3, 1, 2, 3]
|
||||
[22:22] <__m4ch1n3__> !py3 max([i for i in [1,2,3,4,1,2,3,4] if i < 4])
|
||||
[22:22] <codebot> __m4ch1n3__: 3
|
||||
[22:22] <__m4ch1n3__> max(...) would return the maximum of resulting list
|
||||
[22:23] <__m4ch1n3__> !py3 max([i for i in [1, 10, 100] if i < 20])
|
||||
[22:23] <codebot> __m4ch1n3__: 10
|
||||
[22:23] <__m4ch1n3__> !py3 max([i for i in [1, 10, 100] if i < 242])
|
||||
[22:23] <codebot> __m4ch1n3__: 100
|
||||
[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)
|
||||
'''
|
||||
|
||||
#buffer
|
||||
580
pytiled_parser/parser.py
Normal file
580
pytiled_parser/parser.py
Normal file
@@ -0,0 +1,580 @@
|
||||
import functools
|
||||
import re
|
||||
|
||||
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:
|
||||
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
|
||||
27
pytiled_parser/utilities.py
Normal file
27
pytiled_parser/utilities.py
Normal file
@@ -0,0 +1,27 @@
|
||||
import pytiled_parser.objects as objects
|
||||
|
||||
|
||||
def parse_color(color: str) -> objects.Color:
|
||||
"""
|
||||
Converts the color formats that Tiled uses into ones that Arcade accepts.
|
||||
|
||||
Returns:
|
||||
:Color: Color object in the format that Arcade understands.
|
||||
"""
|
||||
# strip initial '#' character
|
||||
if not len(color) % 2 == 0: # pylint: disable=C2001
|
||||
color = color[1:]
|
||||
|
||||
if len(color) == 6:
|
||||
# full opacity if no alpha specified
|
||||
alpha = 0xFF
|
||||
red = int(color[0:2], 16)
|
||||
green = int(color[2:4], 16)
|
||||
blue = int(color[4:6], 16)
|
||||
else:
|
||||
alpha = int(color[0:2], 16)
|
||||
red = int(color[2:4], 16)
|
||||
green = int(color[4:6], 16)
|
||||
blue = int(color[6:8], 16)
|
||||
|
||||
return objects.Color(red, green, blue, alpha)
|
||||
@@ -1,17 +1,19 @@
|
||||
hi
|
||||
{ 'background_color': None,
|
||||
'height': 10,
|
||||
'height': 6,
|
||||
'hex_side_length': None,
|
||||
'infinite': False,
|
||||
'layers': [ Layer(width=10, height=10, data=[[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]], id=1, name='Tile Layer 1', offset=OrderedPair(x=0, y=0), opacity=255, properties=None),
|
||||
Layer(width=10, height=10, data=[[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]], id=2, name='Tile Layer 2', offset=OrderedPair(x=0, y=0), opacity=128, properties=None),
|
||||
None,
|
||||
ObjectGroup(objects=[Object(id=1, location=OrderedPair(x=200.25, y=210.75), size=OrderedPair(x=47.25, y=25.0), rotation=15, opacity=255, name='rectangle 1', type=None, properties=None, template=None), Object(id=2, location=OrderedPair(x=252.5, y=87.75), size=OrderedPair(x=0, y=0), rotation=-21, opacity=255, name='polygon 1', type=None, properties=None, template=None), Object(id=3, location=OrderedPair(x=198.75, y=102.5), size=OrderedPair(x=17.75, y=14.25), rotation=0, opacity=255, name='elipse 1', type=None, properties=None, template=None), Object(id=4, location=OrderedPair(x=174.25, y=186.0), size=OrderedPair(x=0, y=0), rotation=0, opacity=255, name='point 1', type=None, properties=None, template=None), Object(id=7, location=OrderedPair(x=11.3958, y=48.5833), size=OrderedPair(x=107.625, y=27.25), rotation=0, opacity=255, name='insert text 1', type=None, properties=None, template=None), Object(id=6, location=OrderedPair(x=47.25, y=72.5), size=OrderedPair(x=47.0, y=53.0), rotation=31, opacity=255, name='inserted tile 1', type=None, properties={'tile property bool': True}, template=None), Object(id=8, location=OrderedPair(x=144.667, y=112.0), size=OrderedPair(x=0, y=0), rotation=0, opacity=255, name='polyline 1', type=None, properties=None, template=None), Object(id=9, location=OrderedPair(x=69.8333, y=168.333), size=OrderedPair(x=0, y=0), rotation=0, opacity=255, name='polygon 2', type=None, properties=None, template=None)], id=6, name='Object Layer 1', offset=OrderedPair(x=0, y=0), opacity=230, properties=None, color=None, draworder='topdown')],
|
||||
'next_layer_id': 16,
|
||||
'next_object_id': 10,
|
||||
'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)],
|
||||
'next_layer_id': 2,
|
||||
'next_object_id': 1,
|
||||
'orientation': 'orthogonal',
|
||||
'parent_dir': PosixPath('/home/ben/Projects/arcade/arcade-venv/arcade/tests/test_data'),
|
||||
'properties': None,
|
||||
'parent_dir': PosixPath('/home/ben/Projects/pytiled_parser/pytiled_parser-venv/pytiled_parser/tests/test_data'),
|
||||
'properties': { 'bool property - false': False,
|
||||
'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',
|
||||
'stagger_axis': None,
|
||||
'stagger_index': None,
|
||||
@@ -20,4 +22,4 @@ hi
|
||||
'tile_width': 32,
|
||||
'tiled_version': '1.2.3',
|
||||
'version': '1.2',
|
||||
'width': 10}
|
||||
'width': 8}
|
||||
|
||||
@@ -3,16 +3,15 @@ import pickle
|
||||
|
||||
from io import StringIO
|
||||
|
||||
import arcade
|
||||
import arcade.tiled
|
||||
import pytiled_parser
|
||||
|
||||
|
||||
pp = pprint.PrettyPrinter(indent=4, compact=True, width=100)
|
||||
|
||||
pp = pp.pprint
|
||||
|
||||
MAP_NAME = '/home/ben/Projects/arcade/arcade-venv/arcade/tests/test_data/test_map_image_tile_set.tmx'
|
||||
MAP_NAME = '/home/ben/Projects/pytiled_parser/pytiled_parser-venv/pytiled_parser/tests/test_data/test_map_simple.tmx'
|
||||
|
||||
map = arcade.tiled.parse_tile_map(MAP_NAME)
|
||||
map = pytiled_parser.parse_tile_map(MAP_NAME)
|
||||
|
||||
pp(map.__dict__)
|
||||
|
||||
@@ -13,58 +13,72 @@ os.chdir(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
def test_map_simple():
|
||||
"""
|
||||
TMX with a very simple tileset 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"))
|
||||
|
||||
properties = {
|
||||
"bool property - false": False,
|
||||
"bool property - true": True,
|
||||
"color property": (0x49, 0xfc, 0xff, 0xff),
|
||||
"file property": Path("/var/log/syslog"),
|
||||
"float property": 1.23456789,
|
||||
"int property": 13,
|
||||
"string property": "Hello, World!!"
|
||||
}
|
||||
|
||||
# map
|
||||
# unsure how to get paths to compare propperly
|
||||
assert str(map.parent_dir) == "../test_data"
|
||||
assert map.version == "1.2"
|
||||
assert map.tiled_version == "1.2.3"
|
||||
assert map.orientation == "orthogonal"
|
||||
assert map.render_order == "right-down"
|
||||
assert map.width == 8
|
||||
assert map.height == 6
|
||||
assert map.tile_width == 32
|
||||
assert map.tile_height == 32
|
||||
assert map.map_size == (8, 6)
|
||||
assert map.tile_size == (32, 32)
|
||||
assert map.infinite == False
|
||||
assert map.next_layer_id == 2
|
||||
assert map.next_object_id == 1
|
||||
|
||||
# optional, not for orthogonal maps
|
||||
assert map.hex_side_length == None
|
||||
assert map.stagger_axis == None
|
||||
assert map.stagger_index == None
|
||||
assert map.background_color == None
|
||||
assert map.next_layer_id == 2
|
||||
assert map.next_object_id == 1
|
||||
assert map.properties == properties
|
||||
|
||||
assert map.properties == {
|
||||
"bool property - false": False,
|
||||
"bool property - true": True,
|
||||
"color property": (0x49, 0xfc, 0xff, 0xff),
|
||||
"file property": Path("/var/log/syslog"),
|
||||
"float property": 1.23456789,
|
||||
"int property": 13,
|
||||
"string property": "Hello, World!!"
|
||||
}
|
||||
|
||||
# tileset
|
||||
assert map.tile_sets[1].name == "tile_set_image"
|
||||
assert map.tile_sets[1].tilewidth == 32
|
||||
assert map.tile_sets[1].tileheight == 32
|
||||
assert map.tile_sets[1].max_tile_size == (32, 32)
|
||||
assert map.tile_sets[1].spacing == 1
|
||||
assert map.tile_sets[1].margin == 1
|
||||
assert map.tile_sets[1].tilecount == 48
|
||||
assert map.tile_sets[1].tile_count == 48
|
||||
assert map.tile_sets[1].columns == 8
|
||||
assert map.tile_sets[1].tileoffset == None
|
||||
assert map.tile_sets[1].tile_offset == None
|
||||
assert map.tile_sets[1].grid == None
|
||||
assert map.tile_sets[1].properties == None
|
||||
assert map.tile_sets[1].terraintypes == None
|
||||
assert map.tile_sets[1].tiles == {}
|
||||
|
||||
# unsure how to get paths to compare propperly
|
||||
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.width == 265
|
||||
assert map.tile_sets[1].image.height == 199
|
||||
assert map.tile_sets[1].image.size == (265, 199)
|
||||
|
||||
# assert map.layers ==
|
||||
assert map.tile_sets[1].terrain_types == None
|
||||
assert map.tile_sets[1].tiles == {}
|
||||
|
||||
# layers
|
||||
assert map.layers[0].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]]
|
||||
assert map.layers[0].id == 1
|
||||
assert map.layers[0].name == "Tile Layer 1"
|
||||
assert map.layers[0].offset == (0, 0)
|
||||
assert map.layers[0].opacity == 0xFF
|
||||
assert map.layers[0].properties == None
|
||||
assert map.layers[0].size == (8, 6)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -80,4 +94,4 @@ def test_color_parsing(test_input, expected):
|
||||
"""
|
||||
Tiled has a few different types of color representations.
|
||||
"""
|
||||
assert pytiled_parser._parse_color(test_input) == expected
|
||||
assert pytiled_parser.utilities.parse_color(test_input) == expected
|
||||
|
||||
Reference in New Issue
Block a user