fixed image bug

This commit is contained in:
Benjamin Kirkbride
2019-05-06 20:43:22 -04:00
parent 3c8fcdf8e2
commit e8809745be
5 changed files with 442 additions and 214 deletions

View File

@@ -35,6 +35,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
@@ -48,6 +49,7 @@ class OrderedPair(NamedTuple):
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]
@@ -59,6 +61,7 @@ class Size(NamedTuple):
width (Union[int, float]): The width of the object. width (Union[int, float]): The width of the object.
size (Union[int, float]): The height of the object. size (Union[int, float]): The height of the object.
""" """
width: Union[int, float] width: Union[int, float]
height: Union[int, float] height: Union[int, float]
@@ -83,13 +86,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.
@@ -104,9 +109,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: Size 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]]
@@ -120,6 +126,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
@@ -134,6 +141,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
@@ -150,6 +158,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
@@ -168,6 +177,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
@@ -246,13 +256,13 @@ class Layer(LayerType, _LayerBase):
@dataclasses.dataclass @dataclasses.dataclass
class _ObjectBase: class _TiledObjectBase:
id: int id: int
location: OrderedPair location: OrderedPair
@dataclasses.dataclass @dataclasses.dataclass
class _ObjectDefaults: class _TiledObjectDefaults:
size: Size = Size(0, 0) size: Size = Size(0, 0)
rotation: int = 0 rotation: int = 0
opacity: int = 0xFF opacity: int = 0xFF
@@ -265,9 +275,9 @@ class _ObjectDefaults:
@dataclasses.dataclass @dataclasses.dataclass
class Object(_ObjectDefaults, _ObjectBase): class TiledObject(_TiledObjectDefaults, _TiledObjectBase):
""" """
ObjectGroup Object. TiledObjectGroup 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
@@ -284,14 +294,14 @@ class Object(_ObjectDefaults, _ObjectBase):
: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.
@@ -302,7 +312,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.
@@ -311,7 +321,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).
@@ -320,12 +330,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.
@@ -337,12 +347,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.
@@ -354,7 +364,7 @@ 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.
@@ -368,13 +378,13 @@ class PolylineObject(Object, _PointsObjectBase):
@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: Color = Color(0xFF, 0, 0, 0)
@@ -383,12 +393,12 @@ class _TextObjectDefaults(_ObjectDefaults):
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.
@@ -414,19 +424,19 @@ class TextObject(Object, _TextObjectDefaults, _TextObjectBase):
@dataclasses.dataclass @dataclasses.dataclass
class _ObjectGroupBase(_LayerTypeBase): class _ObjectGroupBase(_LayerTypeBase):
objects: List[Object] objects: List[TiledObject]
@dataclasses.dataclass @dataclasses.dataclass
class _ObjectGroupDefaults(_LayerTypeDefaults): class _ObjectGroupDefaults(_LayerTypeDefaults):
color: Optional[Color] = None color: Optional[Color] = None
draw_order: Optional[str] = 'topdown' draw_order: Optional[str] = "topdown"
@dataclasses.dataclass @dataclasses.dataclass
class ObjectGroup(LayerType, _ObjectGroupDefaults, _ObjectGroupBase): 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.
@@ -443,7 +453,8 @@ https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#objectgroup
to 'topdown'. See: 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. :objects (Dict[int, TiledObject]): Dict TiledObject objects by
TiledObject.id.
""" """
@@ -488,12 +499,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]
hitboxes: Optional[List[Object]] hitboxes: Optional[List[TiledObject]]
@dataclasses.dataclass @dataclasses.dataclass
@@ -526,6 +538,7 @@ 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: Size max_tile_size: Size
spacing: Optional[int] spacing: Optional[int]
@@ -581,6 +594,7 @@ class TileMap:
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
@@ -604,7 +618,7 @@ class TileMap:
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]
@@ -620,4 +634,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)
''' """

View File

@@ -60,16 +60,20 @@ def _decode_csv_layer(data_text):
lines = lines[:-1] lines = lines[:-1]
for line in lines: for line in lines:
line_list = line.split(",") line_list = line.split(",")
while '' in line_list: while "" in line_list:
line_list.remove('') line_list.remove("")
line_list_int = [int(item) for item in line_list] line_list_int = [int(item) for item in line_list]
tile_grid.append(line_list_int) tile_grid.append(line_list_int)
return tile_grid return tile_grid
def _decode_data(element: etree.Element, layer_width: int, encoding: str, def _decode_data(
compression: Optional[str]) -> List[List[int]]: element: etree.Element,
layer_width: int,
encoding: str,
compression: Optional[str],
) -> List[List[int]]:
"""Decodes data or chunk data. """Decodes data or chunk data.
Args: Args:
@@ -81,30 +85,29 @@ def _decode_data(element: etree.Element, layer_width: int, encoding: str,
:compression (str): Compression format of the layer data. :compression (str): Compression format of the layer data.
""" """
# etree.Element.text comes with an appended and a prepended '\n' # etree.Element.text comes with an appended and a prepended '\n'
supported_encodings = ['base64', 'csv'] supported_encodings = ["base64", "csv"]
if encoding not in supported_encodings: if encoding not in supported_encodings:
raise ValueError('{encoding} is not a valid encoding') raise ValueError("{encoding} is not a valid encoding")
supported_compression = [None, 'gzip', 'zlib'] supported_compression = [None, "gzip", "zlib"]
if compression is not None: if compression is not None:
if encoding != 'base64': if encoding != "base64":
raise ValueError('{encoding} does not support compression') raise ValueError("{encoding} does not support compression")
if compression not in supported_compression: if compression not in supported_compression:
raise ValueError('{compression} is not a valid compression type') raise ValueError("{compression} is not a valid compression type")
try: try:
data_text = element.text # type: ignore data_text = element.text # type: ignore
except AttributeError: except AttributeError:
raise AttributeError('{element} lacks layer data.') raise AttributeError("{element} lacks layer data.")
if encoding == 'csv': if encoding == "csv":
return _decode_csv_layer(data_text) return _decode_csv_layer(data_text)
return _decode_base64_data(data_text, compression, layer_width) return _decode_base64_data(data_text, compression, layer_width)
def _parse_data(element: etree.Element, def _parse_data(element: etree.Element, layer_width: int) -> objects.LayerData:
layer_width: int) -> objects.LayerData:
"""Parses layer data. """Parses layer data.
Will parse CSV, base64, gzip-base64, or zlip-base64 encoded data. Will parse CSV, base64, gzip-base64, or zlip-base64 encoded data.
@@ -116,103 +119,109 @@ def _parse_data(element: etree.Element,
Returns: Returns:
:LayerData: Data object containing layer data or chunks of data. :LayerData: Data object containing layer data or chunks of data.
""" """
encoding = element.attrib['encoding'] encoding = element.attrib["encoding"]
compression = None compression = None
try: try:
compression = element.attrib['compression'] compression = element.attrib["compression"]
except KeyError: except KeyError:
pass pass
chunk_elements = element.findall('./chunk') chunk_elements = element.findall("./chunk")
if chunk_elements: if chunk_elements:
chunks: List[objects.Chunk] = [] chunks: List[objects.Chunk] = []
for chunk_element in chunk_elements: for chunk_element in chunk_elements:
x = int(chunk_element.attrib['x']) x = int(chunk_element.attrib["x"])
y = int(chunk_element.attrib['y']) y = int(chunk_element.attrib["y"])
location = objects.OrderedPair(x, y) location = objects.OrderedPair(x, y)
width = int(chunk_element.attrib['width']) width = int(chunk_element.attrib["width"])
height = int(chunk_element.attrib['height']) height = int(chunk_element.attrib["height"])
layer_data = _decode_data(chunk_element, layer_width, encoding, layer_data = _decode_data(
compression) chunk_element, layer_width, encoding, compression
)
chunks.append(objects.Chunk(location, width, height, layer_data)) chunks.append(objects.Chunk(location, width, height, layer_data))
return chunks return chunks
return _decode_data(element, layer_width, encoding, compression) return _decode_data(element, layer_width, encoding, compression)
def _parse_layer(element: etree.Element, def _parse_layer(
layer_type: objects.LayerType) -> objects.Layer: element: etree.Element, layer_type: objects.LayerType
) -> objects.Layer:
"""Parse layer element given.""" """Parse layer element given."""
width = int(element.attrib['width']) width = int(element.attrib["width"])
height = int(element.attrib['height']) height = int(element.attrib["height"])
size = objects.OrderedPair(width, height) size = objects.OrderedPair(width, height)
data_element = element.find('./data') data_element = element.find("./data")
if data_element is not None: if data_element is not None:
data: objects.LayerData = _parse_data(data_element, width) data: objects.LayerData = _parse_data(data_element, width)
else: else:
raise ValueError('{element} has no child data element.') raise ValueError("{element} has no child data element.")
return objects.Layer(size, data, **layer_type.__dict__) return objects.Layer(size, data, **layer_type.__dict__)
def _parse_layer_type(layer_element: etree.Element) -> objects.LayerType: def _parse_layer_type(layer_element: etree.Element) -> objects.LayerType:
"""Parse layer type element given.""" """Parse layer type element given."""
id = int(layer_element.attrib['id']) id = int(layer_element.attrib["id"])
name = layer_element.attrib['name'] name = layer_element.attrib["name"]
layer_type_object = objects.LayerType(id, name) layer_type_object = objects.LayerType(id, name)
try: try:
offset_x = float(layer_element.attrib['offsetx']) offset_x = float(layer_element.attrib["offsetx"])
except KeyError: except KeyError:
offset_x = 0 offset_x = 0
try: try:
offset_y = float(layer_element.attrib['offsety']) offset_y = float(layer_element.attrib["offsety"])
except KeyError: except KeyError:
offset_y = 0 offset_y = 0
offset = objects.OrderedPair(offset_x, offset_y) offset = objects.OrderedPair(offset_x, offset_y)
try: try:
layer_type_object.opacity = round( layer_type_object.opacity = round(
float(layer_element.attrib['opacity']) * 255) float(layer_element.attrib["opacity"]) * 255
)
except KeyError: except KeyError:
pass pass
properties_element = layer_element.find('./properties') properties_element = layer_element.find("./properties")
if properties_element is not None: if properties_element is not None:
layer_type_object.properties = _parse_properties_element( layer_type_object.properties = _parse_properties_element(
properties_element) properties_element
)
if layer_element.tag == 'layer': if layer_element.tag == "layer":
return _parse_layer(layer_element, layer_type_object) return _parse_layer(layer_element, layer_type_object)
elif layer_element.tag == 'objectgroup': elif layer_element.tag == "objectgroup":
return _parse_object_group(layer_element, layer_type_object) return _parse_object_group(layer_element, layer_type_object)
# else: # else:
# return _parse_layer_group(layer_element, layer_type_object) # return _parse_layer_group(layer_element, layer_type_object)
def _parse_objects(object_elements: List[etree.Element]) -> List[objects.Object]: def _parse_objects(
object_elements: List[etree.Element]
) -> List[objects.TiledObject]:
""" """
""" """
tiled_objects: List[objects.Object] = [] tiled_objects: List[objects.TiledObject] = []
for object_element in object_elements: for object_element in object_elements:
id = int(object_element.attrib['id']) id = int(object_element.attrib["id"])
location_x = float(object_element.attrib['x']) location_x = float(object_element.attrib["x"])
location_y = float(object_element.attrib['y']) location_y = float(object_element.attrib["y"])
location = objects.OrderedPair(location_x, location_y) location = objects.OrderedPair(location_x, location_y)
object = objects.Object(id, location) object = objects.TiledObject(id, location)
try: try:
width = float(object_element.attrib['width']) width = float(object_element.attrib["width"])
except KeyError: except KeyError:
width = 0 width = 0
try: try:
height = float(object_element.attrib['height']) height = float(object_element.attrib["height"])
except KeyError: except KeyError:
height = 0 height = 0
@@ -220,21 +229,22 @@ def _parse_objects(object_elements: List[etree.Element]) -> List[objects.Object]
try: try:
object.opacity = round( object.opacity = round(
float(object_element.attrib['opacity']) * 255) float(object_element.attrib["opacity"]) * 255
)
except KeyError: except KeyError:
pass pass
try: try:
object.rotation = int(object_element.attrib['rotation']) object.rotation = int(object_element.attrib["rotation"])
except KeyError: except KeyError:
pass pass
try: try:
object.name = object_element.attrib['name'] object.name = object_element.attrib["name"]
except KeyError: except KeyError:
pass pass
properties_element = object_element.find('./properties') properties_element = object_element.find("./properties")
if properties_element is not None: if properties_element is not None:
object.properties = _parse_properties_element(properties_element) object.properties = _parse_properties_element(properties_element)
@@ -243,23 +253,24 @@ def _parse_objects(object_elements: List[etree.Element]) -> List[objects.Object]
return tiled_objects return tiled_objects
def _parse_object_group(element: etree.Element, def _parse_object_group(
layer_type: objects.LayerType) -> objects.ObjectGroup: element: etree.Element, layer_type: objects.LayerType
) -> objects.ObjectGroup:
"""Parse the objectgroup element given. """Parse the objectgroup element given.
Args: Args:
layer_type (objects.LayerType): layer_type (objects.LayerType):
""" """
tiled_objects = _parse_objects(element.findall('./object')) tiled_objects = _parse_objects(element.findall("./object"))
object_group = objects.ObjectGroup(tiled_objects, **layer_type.__dict__) object_group = objects.ObjectGroup(tiled_objects, **layer_type.__dict__)
try: try:
color = utilities.parse_color(element.attrib['color']) color = utilities.parse_color(element.attrib["color"])
except KeyError: except KeyError:
pass pass
try: try:
draw_order = element.attrib['draworder'] draw_order = element.attrib["draworder"]
except KeyError: except KeyError:
pass pass
@@ -267,85 +278,88 @@ def _parse_object_group(element: etree.Element,
@functools.lru_cache() @functools.lru_cache()
def _parse_external_tile_set(parent_dir: Path, tile_set_element: etree.Element def _parse_external_tile_set(
) -> objects.TileSet: parent_dir: Path, tile_set_element: etree.Element
) -> objects.TileSet:
"""Parses an external tile set. """Parses an external tile set.
Caches the results to speed up subsequent instances. Caches the results to speed up subsequent instances.
""" """
source = Path(tile_set_element.attrib['source']) source = Path(tile_set_element.attrib["source"])
tile_set_tree = etree.parse(str(parent_dir / Path(source))).getroot() tile_set_tree = etree.parse(str(parent_dir / Path(source))).getroot()
return _parse_tile_set(tile_set_tree) return _parse_tile_set(tile_set_tree)
def _parse_hitboxes(element: etree.Element) -> List[objects.Object]: def _parse_hitboxes(element: etree.Element) -> List[objects.TiledObject]:
return _parse_objects(element.findall('./object')) return _parse_objects(element.findall("./object"))
def _parse_tiles(tile_element_list: List[etree.Element] def _parse_tiles(
) -> Dict[int, objects.Tile]: tile_element_list: List[etree.Element]
) -> Dict[int, objects.Tile]:
tiles: Dict[int, objects.Tile] = {} tiles: Dict[int, objects.Tile] = {}
for tile_element in tile_element_list: for tile_element in tile_element_list:
#id is not optional # id is not optional
id = int(tile_element.attrib['id']) id = int(tile_element.attrib["id"])
#optional attributes # optional attributes
tile_type = None tile_type = None
try: try:
tile_type = tile_element.attrib['type'] tile_type = tile_element.attrib["type"]
except KeyError: except KeyError:
pass pass
tile_terrain = None tile_terrain = None
try: try:
tile_terrain_attrib = tile_element.attrib['terrain'] tile_terrain_attrib = tile_element.attrib["terrain"]
except KeyError: except KeyError:
pass pass
else: else:
#below is an attempt to explain how terrains are handled. # below is an attempt to explain how terrains are handled.
#'terrain' attribute is a comma seperated list of 4 values, #'terrain' attribute is a comma seperated list of 4 values,
#each is either an integer or blank # each is either an integer or blank
#convert to list of values # convert to list of values
terrain_list_attrib = re.split(',', tile_terrain_attrib) terrain_list_attrib = re.split(",", tile_terrain_attrib)
#terrain_list is list of indexes of Tileset.terrain_types # terrain_list is list of indexes of Tileset.terrain_types
terrain_list: List[Optional[int]] = [] terrain_list: List[Optional[int]] = []
#each index in terrain_list_attrib reffers to a corner # each index in terrain_list_attrib reffers to a corner
for corner in terrain_list_attrib: for corner in terrain_list_attrib:
if corner == '': if corner == "":
terrain_list.append(None) terrain_list.append(None)
else: else:
terrain_list.append(int(corner)) terrain_list.append(int(corner))
tile_terrain = objects.TileTerrain(*terrain_list) tile_terrain = objects.TileTerrain(*terrain_list)
#tile element optional sub-elements # tile element optional sub-elements
animation: Optional[List[objects.Frame]] = None animation: Optional[List[objects.Frame]] = None
tile_animation_element = tile_element.find('./animation') tile_animation_element = tile_element.find("./animation")
if tile_animation_element: if tile_animation_element:
animation = [] animation = []
frames = tile_animation_element.findall('./frame') frames = tile_animation_element.findall("./frame")
for frame in frames: for frame in frames:
# tileid reffers to the Tile.id of the animation frame # tileid reffers to the Tile.id of the animation frame
tile_id = int(frame.attrib['tileid']) tile_id = int(frame.attrib["tileid"])
# duration is in MS. Should perhaps be converted to seconds. # duration is in MS. Should perhaps be converted to seconds.
# FIXME: make decision # FIXME: make decision
duration = int(frame.attrib['duration']) duration = int(frame.attrib["duration"])
animation.append(objects.Frame(tile_id, duration)) animation.append(objects.Frame(tile_id, duration))
# if this is None, then the Tile is part of a spritesheet # if this is None, then the Tile is part of a spritesheet
tile_image = None tile_image = None
tile_image_element = tile_element.find('./image') tile_image_element = tile_element.find("./image")
if tile_image_element is not None: if tile_image_element is not None:
tile_image = _parse_image_element(tile_image_element) tile_image = _parse_image_element(tile_image_element)
hitboxes = None hitboxes = None
tile_hitboxes_element = tile_element.find('./objectgroup') tile_hitboxes_element = tile_element.find("./objectgroup")
if tile_hitboxes_element is not None: if tile_hitboxes_element is not None:
hitboxes = _parse_hitboxes(tile_hitboxes_element) hitboxes = _parse_hitboxes(tile_hitboxes_element)
tiles[id] = objects.Tile(id, tile_type, tile_terrain, animation, tiles[id] = objects.Tile(
tile_image, hitboxes) id, tile_type, tile_terrain, animation, tile_image, hitboxes
)
return tiles return tiles
@@ -356,23 +370,25 @@ def _parse_image_element(image_element: etree.Element) -> objects.Image:
Returns: Returns:
:Color: Color in Arcade's preffered format. :Color: Color in Arcade's preffered format.
""" """
source = image_element.attrib['source'] 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))
trans = None
try: try:
trans = utilities.parse_color(image_element.attrib['trans']) image.trans = utilities.parse_color(image_element.attrib["trans"])
except KeyError: except KeyError:
pass pass
width = int(image_element.attrib['width']) return image
height = int(image_element.attrib['height'])
size = objects.Size(width, height)
return objects.Image(source, size, trans)
def _parse_properties_element(properties_element: etree.Element def _parse_properties_element(
) -> objects.Properties: properties_element: etree.Element
) -> objects.Properties:
"""Adds Tiled property to Properties dict. """Adds Tiled property to Properties dict.
Args: Args:
@@ -385,29 +401,30 @@ def _parse_properties_element(properties_element: etree.Element
:Properties: Properties Dict object. :Properties: Properties Dict object.
""" """
properties: objects.Properties = {} properties: objects.Properties = {}
for property_element in properties_element.findall('./property'): for property_element in properties_element.findall("./property"):
name = property_element.attrib['name'] name = property_element.attrib["name"]
try: try:
property_type = property_element.attrib['type'] property_type = property_element.attrib["type"]
except KeyError: except KeyError:
# strings do not have an attribute in property elements # strings do not have an attribute in property elements
property_type = 'string' property_type = "string"
value = property_element.attrib['value'] value = property_element.attrib["value"]
property_types = ['string', 'int', 'float', 'bool', 'color', 'file'] property_types = ["string", "int", "float", "bool", "color", "file"]
assert property_type in property_types, ( assert (
f"Invalid type for property {name}") property_type in property_types
), f"Invalid type for property {name}"
if property_type == 'int': if property_type == "int":
properties[name] = int(value) properties[name] = int(value)
elif property_type == 'float': elif property_type == "float":
properties[name] = float(value) properties[name] = float(value)
elif property_type == 'color': elif property_type == "color":
properties[name] = utilities.parse_color(value) properties[name] = utilities.parse_color(value)
elif property_type == 'file': elif property_type == "file":
properties[name] = Path(value) properties[name] = Path(value)
elif property_type == 'bool': elif property_type == "bool":
if value == 'true': if value == "true":
properties[name] = True properties[name] = True
else: else:
properties[name] = False properties[name] = False
@@ -422,75 +439,86 @@ def _parse_tile_set(tile_set_element: etree.Element) -> objects.TileSet:
Parses a tile set that is embedded into a TMX. Parses a tile set that is embedded into a TMX.
""" """
# get all basic attributes # get all basic attributes
name = tile_set_element.attrib['name'] name = tile_set_element.attrib["name"]
max_tile_width = int(tile_set_element.attrib['tilewidth']) max_tile_width = int(tile_set_element.attrib["tilewidth"])
max_tile_height = int(tile_set_element.attrib['tileheight']) max_tile_height = int(tile_set_element.attrib["tileheight"])
max_tile_size = objects.OrderedPair(max_tile_width, max_tile_height) max_tile_size = objects.OrderedPair(max_tile_width, max_tile_height)
spacing = None spacing = None
try: try:
spacing = int(tile_set_element.attrib['spacing']) spacing = int(tile_set_element.attrib["spacing"])
except KeyError: except KeyError:
pass pass
margin = None margin = None
try: try:
margin = int(tile_set_element.attrib['margin']) margin = int(tile_set_element.attrib["margin"])
except KeyError: except KeyError:
pass pass
tile_count = None tile_count = None
try: try:
tile_count = int(tile_set_element.attrib['tilecount']) tile_count = int(tile_set_element.attrib["tilecount"])
except KeyError: except KeyError:
pass pass
columns = None columns = None
try: try:
columns = int(tile_set_element.attrib['columns']) columns = int(tile_set_element.attrib["columns"])
except KeyError: except KeyError:
pass pass
tile_offset = None tile_offset = None
tileoffset_element = tile_set_element.find('./tileoffset') tileoffset_element = tile_set_element.find("./tileoffset")
if tileoffset_element is not None: if tileoffset_element is not None:
tile_offset_x = int(tileoffset_element.attrib['x']) tile_offset_x = int(tileoffset_element.attrib["x"])
tile_offset_y = int(tileoffset_element.attrib['y']) tile_offset_y = int(tileoffset_element.attrib["y"])
tile_offset = objects.OrderedPair(tile_offset_x, tile_offset_y) tile_offset = objects.OrderedPair(tile_offset_x, tile_offset_y)
grid = None grid = None
grid_element = tile_set_element.find('./grid') grid_element = tile_set_element.find("./grid")
if grid_element is not None: if grid_element is not None:
grid_orientation = grid_element.attrib['orientation'] grid_orientation = grid_element.attrib["orientation"]
grid_width = int(grid_element.attrib['width']) grid_width = int(grid_element.attrib["width"])
grid_height = int(grid_element.attrib['height']) grid_height = int(grid_element.attrib["height"])
grid = objects.Grid(grid_orientation, grid_width, grid_height) grid = objects.Grid(grid_orientation, grid_width, grid_height)
properties = None properties = None
properties_element = tile_set_element.find('./properties') properties_element = tile_set_element.find("./properties")
if properties_element is not None: if properties_element is not None:
properties = _parse_properties_element(properties_element) properties = _parse_properties_element(properties_element)
terrain_types: Optional[List[objects.Terrain]] = None terrain_types: Optional[List[objects.Terrain]] = None
terrain_types_element = tile_set_element.find('./terraintypes') terrain_types_element = tile_set_element.find("./terraintypes")
if terrain_types_element is not None: if terrain_types_element is not None:
terrain_types = [] terrain_types = []
for terrain in terrain_types_element.findall('./terrain'): for terrain in terrain_types_element.findall("./terrain"):
name = terrain.attrib['name'] name = terrain.attrib["name"]
terrain_tile = int(terrain.attrib['tile']) terrain_tile = int(terrain.attrib["tile"])
terrain_types.append(objects.Terrain(name, terrain_tile)) terrain_types.append(objects.Terrain(name, terrain_tile))
image = None image = None
image_element = tile_set_element.find('./image') image_element = tile_set_element.find("./image")
if image_element is not None: if image_element is not None:
image = _parse_image_element(image_element) image = _parse_image_element(image_element)
tile_element_list = tile_set_element.findall('./tile') tile_element_list = tile_set_element.findall("./tile")
tiles = _parse_tiles(tile_element_list) tiles = _parse_tiles(tile_element_list)
return objects.TileSet(name, max_tile_size, spacing, margin, tile_count, return objects.TileSet(
columns, tile_offset, grid, properties, image, name,
terrain_types, tiles) 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]): def parse_tile_map(tmx_file: Union[str, Path]):
@@ -501,26 +529,26 @@ def parse_tile_map(tmx_file: Union[str, Path]):
# positional arguments for TileMap # positional arguments for TileMap
parent_dir = Path(tmx_file).parent parent_dir = Path(tmx_file).parent
version = map_element.attrib['version'] version = map_element.attrib["version"]
tiled_version = map_element.attrib['tiledversion'] tiled_version = map_element.attrib["tiledversion"]
orientation = map_element.attrib['orientation'] orientation = map_element.attrib["orientation"]
render_order = map_element.attrib['renderorder'] render_order = map_element.attrib["renderorder"]
map_width = int(map_element.attrib['width']) map_width = int(map_element.attrib["width"])
map_height = int(map_element.attrib['height']) map_height = int(map_element.attrib["height"])
map_size = objects.OrderedPair(map_width, map_height) map_size = objects.OrderedPair(map_width, map_height)
tile_width = int(map_element.attrib['tilewidth']) tile_width = int(map_element.attrib["tilewidth"])
tile_height = int(map_element.attrib['tileheight']) tile_height = int(map_element.attrib["tileheight"])
tile_size = objects.OrderedPair(tile_width, tile_height) tile_size = objects.OrderedPair(tile_width, tile_height)
infinite_attribute = map_element.attrib['infinite'] infinite_attribute = map_element.attrib["infinite"]
infinite = True if infinite_attribute == 'true' else False infinite = True if infinite_attribute == "true" else False
next_layer_id = int(map_element.attrib['nextlayerid']) next_layer_id = int(map_element.attrib["nextlayerid"])
next_object_id = int(map_element.attrib['nextobjectid']) next_object_id = int(map_element.attrib["nextobjectid"])
# parse all tilesets # parse all tilesets
tile_sets: Dict[int, objects.TileSet] = {} tile_sets: Dict[int, objects.TileSet] = {}
tile_set_element_list = map_element.findall('./tileset') tile_set_element_list = map_element.findall("./tileset")
for tile_set_element in tile_set_element_list: for tile_set_element in tile_set_element_list:
# tiled docs are ambiguous about the 'firstgid' attribute # tiled docs are ambiguous about the 'firstgid' attribute
# current understanding is for the purposes of mapping the layer # current understanding is for the purposes of mapping the layer
@@ -529,56 +557,67 @@ def parse_tile_map(tmx_file: Union[str, Path]):
# tile set as they pertain to the map, not tile set specific as # tile set as they pertain to the map, not tile set specific as
# the tiled docs can make it seem # the tiled docs can make it seem
# 'firstgid' is saved beside each TileMap # 'firstgid' is saved beside each TileMap
firstgid = int(tile_set_element.attrib['firstgid']) firstgid = int(tile_set_element.attrib["firstgid"])
try: try:
# check if is an external TSX # check if is an external TSX
source = tile_set_element.attrib['source'] source = tile_set_element.attrib["source"]
except KeyError: except KeyError:
# the tile set in embedded # the tile set in embedded
name = tile_set_element.attrib['name'] name = tile_set_element.attrib["name"]
tile_sets[firstgid] = _parse_tile_set(tile_set_element) tile_sets[firstgid] = _parse_tile_set(tile_set_element)
else: else:
# tile set is external # tile set is external
tile_sets[firstgid] = _parse_external_tile_set( tile_sets[firstgid] = _parse_external_tile_set(
parent_dir, tile_set_element) parent_dir, tile_set_element
)
# parse all layers # parse all layers
layers: List[objects.LayerType] = [] layers: List[objects.LayerType] = []
layer_tags = ['layer', 'objectgroup', 'group'] layer_tags = ["layer", "objectgroup", "group"]
for element in map_element.findall('./'): for element in map_element.findall("./"):
if element.tag not in layer_tags: if element.tag not in layer_tags:
# only layer_tags are layer elements # only layer_tags are layer elements
continue continue
layers.append(_parse_layer_type(element)) layers.append(_parse_layer_type(element))
tile_map = objects.TileMap(parent_dir, version, tiled_version, tile_map = objects.TileMap(
orientation, render_order, map_size, tile_size, parent_dir,
infinite, next_layer_id, next_object_id, version,
tile_sets, layers) tiled_version,
orientation,
render_order,
map_size,
tile_size,
infinite,
next_layer_id,
next_object_id,
tile_sets,
layers,
)
try: try:
tile_map.hex_side_length = int(map_element.attrib['hexsidelength']) tile_map.hex_side_length = int(map_element.attrib["hexsidelength"])
except KeyError: except KeyError:
pass pass
try: try:
tile_map.stagger_axis = int(map_element.attrib['staggeraxis']) tile_map.stagger_axis = int(map_element.attrib["staggeraxis"])
except KeyError: except KeyError:
pass pass
try: try:
tile_map.stagger_index = int(map_element.attrib['staggerindex']) tile_map.stagger_index = int(map_element.attrib["staggerindex"])
except KeyError: except KeyError:
pass pass
try: try:
backgroundcolor = map_element.attrib['backgroundcolor'] backgroundcolor = map_element.attrib["backgroundcolor"]
except KeyError: except KeyError:
pass pass
else: else:
tile_map.background_color = utilities.parse_color(backgroundcolor) tile_map.background_color = utilities.parse_color(backgroundcolor)
properties_element = map_tree.find('./properties') properties_element = map_tree.find("./properties")
if properties_element is not None: if properties_element is not None:
tile_map.properties = _parse_properties_element(properties_element) tile_map.properties = _parse_properties_element(properties_element)

View File

@@ -1,25 +1,177 @@
{ 'background_color': None, {
'height': 6, "background_color": None,
'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": [
'next_layer_id': 2, Layer(
'next_object_id': 1, size=OrderedPair(x=8, y=6),
'orientation': 'orthogonal', data=[
'parent_dir': PosixPath('/home/ben/Projects/pytiled_parser/pytiled_parser-venv/pytiled_parser/tests/test_data'), [1, 2, 3, 4, 5, 6, 7, 8],
'properties': { 'bool property - false': False, [9, 10, 11, 12, 13, 14, 15, 16],
'bool property - true': True, [17, 18, 19, 20, 21, 22, 23, 24],
'color property': Color(red=73, green=252, blue=255, alpha=255), [25, 26, 27, 28, 29, 30, 31, 32],
'file property': PosixPath('/var/log/syslog'), [33, 34, 35, 36, 37, 38, 39, 40],
'float property': 1.23456789, [41, 42, 43, 44, 45, 46, 47, 48],
'int property': 13, ],
'string property': 'Hello, World!!'}, id=1,
'render_order': 'right-down', name="Tile Layer 1",
'stagger_axis': None, offset=OrderedPair(x=0, y=0),
'stagger_index': None, opacity=255,
'tile_height': 32, properties=None,
'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_width': 32, ],
'tiled_version': '1.2.3', "map_size": OrderedPair(x=8, y=6),
'version': '1.2', "next_layer_id": 2,
'width': 8} "next_object_id": 1,
"orientation": "orthogonal",
"parent_dir": PosixPath(
"/home/benk/Projects/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,
"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",
size=Size(width=265, height=199),
trans=None,
),
terrain_types=None,
tiles={
9: Tile(
id=9,
type=None,
terrain=None,
animation=None,
image=None,
hitboxes=[
Object(
id=2,
location=OrderedPair(x=1.0, y=1.0),
size=Size(width=32.0, height=32.0),
rotation=1,
opacity=255,
name="wall",
type=None,
properties=None,
template=None,
)
],
),
19: Tile(
id=19,
type=None,
terrain=None,
animation=None,
image=None,
hitboxes=[
Object(
id=1,
location=OrderedPair(x=32.0, y=1.0),
size=Size(width=0, height=0),
rotation=1,
opacity=255,
name="wall corner",
type=None,
properties=None,
template=None,
)
],
),
20: Tile(
id=20,
type=None,
terrain=None,
animation=None,
image=None,
hitboxes=[
Object(
id=1,
location=OrderedPair(x=1.45455, y=1.45455),
size=Size(width=0, height=0),
rotation=1,
opacity=255,
name="polyline",
type=None,
properties=None,
template=None,
)
],
),
31: Tile(
id=31,
type=None,
terrain=None,
animation=None,
image=None,
hitboxes=[
Object(
id=1,
location=OrderedPair(x=5.09091, y=2.54545),
size=Size(width=19.6364, height=19.2727),
rotation=1,
opacity=255,
name="rock 1",
type=None,
properties=None,
template=None,
),
Object(
id=2,
location=OrderedPair(x=16.1818, y=22.0),
size=Size(width=8.54545, height=8.36364),
rotation=-1,
opacity=255,
name="rock 2",
type=None,
properties=None,
template=None,
),
],
),
45: Tile(
id=45,
type=None,
terrain=None,
animation=None,
image=None,
hitboxes=[
Object(
id=1,
location=OrderedPair(x=14.7273, y=26.3636),
size=Size(width=0, height=0),
rotation=0,
opacity=255,
name="sign",
type=None,
properties=None,
template=None,
)
],
),
},
)
},
"tile_size": OrderedPair(x=32, y=32),
"tiled_version": "1.2.3",
"version": "1.2",
}

View File

@@ -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_simple_hitboxes.tmx'
map = pytiled_parser.parse_tile_map(MAP_NAME) map = pytiled_parser.parse_tile_map(MAP_NAME)

View 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>