|
@@ -0,0 +1,917 @@
|
|
|
+# -*- coding: utf-8 -*-
|
|
|
+
|
|
|
+"""
|
|
|
+TileMap loader for python for Tiled, a generic tile map editor
|
|
|
+from http://mapeditor.org/ .
|
|
|
+It loads the \*.tmx files produced by Tiled.
|
|
|
+
|
|
|
+
|
|
|
+"""
|
|
|
+
|
|
|
+# Versioning scheme based on: http://en.wikipedia.org/wiki/Versioning#Designating_development_stage
|
|
|
+#
|
|
|
+# +-- api change, probably incompatible with older versions
|
|
|
+# | +-- enhancements but no api change
|
|
|
+# | |
|
|
|
+# major.minor[.build[.revision]]
|
|
|
+# |
|
|
|
+# +-|* 0 for alpha (status)
|
|
|
+# |* 1 for beta (status)
|
|
|
+# |* 2 for release candidate
|
|
|
+# |* 3 for (public) release
|
|
|
+#
|
|
|
+# For instance:
|
|
|
+# * 1.2.0.1 instead of 1.2-a
|
|
|
+# * 1.2.1.2 instead of 1.2-b2 (beta with some bug fixes)
|
|
|
+# * 1.2.2.3 instead of 1.2-rc (release candidate)
|
|
|
+# * 1.2.3.0 instead of 1.2-r (commercial distribution)
|
|
|
+# * 1.2.3.5 instead of 1.2-r5 (commercial distribution with many bug fixes)
|
|
|
+
|
|
|
+__revision__ = "$Rev: 115 $"
|
|
|
+__version__ = "3.1.0." + __revision__[6:-2]
|
|
|
+__author__ = 'DR0ID @ 2009-2011'
|
|
|
+
|
|
|
+# import logging
|
|
|
+# #the following few lines are needed to use logging if this module used without
|
|
|
+# # a previous call to logging.basicConfig()
|
|
|
+# if 0 == len(logging.root.handlers):
|
|
|
+ # logging.basicConfig(level=logging.DEBUG)
|
|
|
+
|
|
|
+# _LOGGER = logging.getLogger('tiledtmxloader')
|
|
|
+# if __debug__:
|
|
|
+ # _LOGGER.debug('%s loading ...' % (__name__))
|
|
|
+# -----------------------------------------------------------------------------
|
|
|
+
|
|
|
+
|
|
|
+import sys
|
|
|
+from xml.dom import minidom, Node
|
|
|
+try:
|
|
|
+ import StringIO
|
|
|
+ from StringIO import StringIO
|
|
|
+except:
|
|
|
+ from io import StringIO
|
|
|
+import os.path
|
|
|
+import struct
|
|
|
+import array
|
|
|
+
|
|
|
+# -----------------------------------------------------------------------------
|
|
|
+class TileMap(object):
|
|
|
+ """
|
|
|
+
|
|
|
+ The TileMap holds all the map data.
|
|
|
+
|
|
|
+ :Ivariables:
|
|
|
+ orientation : string
|
|
|
+ orthogonal or isometric or hexagonal or shifted
|
|
|
+ tilewidth : int
|
|
|
+ width of the tiles (for all layers)
|
|
|
+ tileheight : int
|
|
|
+ height of the tiles (for all layers)
|
|
|
+ width : int
|
|
|
+ width of the map (number of tiles)
|
|
|
+ height : int
|
|
|
+ height of the map (number of tiles)
|
|
|
+ version : string
|
|
|
+ version of the map format
|
|
|
+ tile_sets : list
|
|
|
+ list of TileSet
|
|
|
+ properties : dict
|
|
|
+ the propertis set in the editor, name-value pairs, strings
|
|
|
+ pixel_width : int
|
|
|
+ width of the map in pixels
|
|
|
+ pixel_height : int
|
|
|
+ height of the map in pixels
|
|
|
+ layers : list
|
|
|
+ list of TileLayer
|
|
|
+ map_file_name : dict
|
|
|
+ file name of the map
|
|
|
+ named_layers : dict of string:TledLayer
|
|
|
+ dict containing {name : TileLayer}
|
|
|
+ named_tile_sets : dict
|
|
|
+ dict containing {name : TileSet}
|
|
|
+
|
|
|
+ """
|
|
|
+
|
|
|
+
|
|
|
+ def __init__(self):
|
|
|
+# This is the top container for all data. The gid is the global id
|
|
|
+# (for a image).
|
|
|
+# Before calling convert most of the values are strings. Some additional
|
|
|
+# values are also calculated, see convert() for details. After calling
|
|
|
+# convert, most values are integers or floats where appropriat.
|
|
|
+ """
|
|
|
+ The TileMap holds all the map data.
|
|
|
+ """
|
|
|
+ # set through parser
|
|
|
+ self.orientation = None
|
|
|
+ self.tileheight = 0
|
|
|
+ self.tilewidth = 0
|
|
|
+ self.width = 0
|
|
|
+ self.height = 0
|
|
|
+ self.version = 0
|
|
|
+ self.tile_sets = [] # TileSet
|
|
|
+ # ISSUE 9: object groups should be in the same order as layers
|
|
|
+ self.layers = [] # WorldTileLayer <- what order? back to front (guessed)
|
|
|
+ # self.object_groups = []
|
|
|
+ self.properties = {} # {name: value}
|
|
|
+ # additional info
|
|
|
+ self.pixel_width = 0
|
|
|
+ self.pixel_height = 0
|
|
|
+ self.named_layers = {} # {name: layer}
|
|
|
+ self.named_tile_sets = {} # {name: tile_set}
|
|
|
+ self.map_file_name = ""
|
|
|
+
|
|
|
+ def convert(self):
|
|
|
+ """
|
|
|
+ Converts numerical values from strings to numerical values.
|
|
|
+ It also calculates or set additional data:
|
|
|
+ pixel_width
|
|
|
+ pixel_height
|
|
|
+ named_layers
|
|
|
+ named_tile_sets
|
|
|
+ """
|
|
|
+ self.tilewidth = int(self.tilewidth)
|
|
|
+ self.tileheight = int(self.tileheight)
|
|
|
+ self.width = int(self.width)
|
|
|
+ self.height = int(self.height)
|
|
|
+ self.pixel_width = self.width * self.tilewidth
|
|
|
+ self.pixel_height = self.height * self.tileheight
|
|
|
+
|
|
|
+ for layer in self.layers:
|
|
|
+ # ISSUE 9
|
|
|
+ if not layer.is_object_group:
|
|
|
+ layer.tilewidth = self.tilewidth
|
|
|
+ layer.tileheight = self.tileheight
|
|
|
+ self.named_layers[layer.name] = layer
|
|
|
+ layer.convert()
|
|
|
+
|
|
|
+ for tile_set in self.tile_sets:
|
|
|
+ self.named_tile_sets[tile_set.name] = tile_set
|
|
|
+ tile_set.spacing = int(tile_set.spacing)
|
|
|
+ tile_set.margin = int(tile_set.margin)
|
|
|
+ for img in tile_set.images:
|
|
|
+ if img.trans:
|
|
|
+ img.trans = (int(img.trans[:2], 16), \
|
|
|
+ int(img.trans[2:4], 16), \
|
|
|
+ int(img.trans[4:], 16))
|
|
|
+
|
|
|
+ def decode(self):
|
|
|
+ """
|
|
|
+ Decodes the TileLayer encoded_content and saves it in decoded_content.
|
|
|
+ """
|
|
|
+ for layer in self.layers:
|
|
|
+ if not layer.is_object_group:
|
|
|
+ layer.decode()
|
|
|
+# -----------------------------------------------------------------------------
|
|
|
+
|
|
|
+
|
|
|
+class TileSet(object):
|
|
|
+ """
|
|
|
+ A tileset holds the tiles and its images.
|
|
|
+
|
|
|
+ :Ivariables:
|
|
|
+ firstgid : int
|
|
|
+ the first gid of this tileset
|
|
|
+ name : string
|
|
|
+ the name of this TileSet
|
|
|
+ images : list
|
|
|
+ list of TileImages
|
|
|
+ tiles : list
|
|
|
+ list of Tiles
|
|
|
+ indexed_images : dict
|
|
|
+ after calling load() it is dict containing id: image
|
|
|
+ spacing : int
|
|
|
+ the spacing between tiles
|
|
|
+ marging : int
|
|
|
+ the marging of the tiles
|
|
|
+ properties : dict
|
|
|
+ the propertis set in the editor, name-value pairs
|
|
|
+ tilewidth : int
|
|
|
+ the actual width of the tile, can be different from the tilewidth
|
|
|
+ of the map
|
|
|
+ tilehight : int
|
|
|
+ the actual hight of th etile, can be different from the tilehight
|
|
|
+ of the map
|
|
|
+
|
|
|
+ """
|
|
|
+
|
|
|
+ def __init__(self):
|
|
|
+ self.firstgid = 0
|
|
|
+ self.name = None
|
|
|
+ self.images = [] # TileImage
|
|
|
+ self.tiles = [] # Tile
|
|
|
+ self.indexed_images = {} # {id:image}
|
|
|
+ self.spacing = 0
|
|
|
+ self.margin = 0
|
|
|
+ self.properties = {}
|
|
|
+ self.tileheight = 0
|
|
|
+ self.tilewidth = 0
|
|
|
+
|
|
|
+# -----------------------------------------------------------------------------
|
|
|
+
|
|
|
+class TileImage(object):
|
|
|
+ """
|
|
|
+ An image of a tile or just an image.
|
|
|
+
|
|
|
+ :Ivariables:
|
|
|
+ id : int
|
|
|
+ id of this image (has nothing to do with gid)
|
|
|
+ format : string
|
|
|
+ the format as string, only 'png' at the moment
|
|
|
+ source : string
|
|
|
+ filename of the image. either this is set or the content
|
|
|
+ encoding : string
|
|
|
+ encoding of the content
|
|
|
+ trans : tuple of (r,g,b)
|
|
|
+ the colorkey color, raw as hex, after calling convert just a
|
|
|
+ (r,g,b) tuple
|
|
|
+ properties : dict
|
|
|
+ the propertis set in the editor, name-value pairs
|
|
|
+ image : TileImage
|
|
|
+ after calling load the pygame surface
|
|
|
+ """
|
|
|
+
|
|
|
+ def __init__(self):
|
|
|
+ self.id = 0
|
|
|
+ self.format = None
|
|
|
+ self.source = None
|
|
|
+ self.encoding = None # from <data>...</data>
|
|
|
+ self.content = None # from <data>...</data>
|
|
|
+ self.image = None
|
|
|
+ self.trans = None
|
|
|
+ self.properties = {} # {name: value}
|
|
|
+
|
|
|
+# -----------------------------------------------------------------------------
|
|
|
+
|
|
|
+class Tile(object):
|
|
|
+ """
|
|
|
+ A single tile.
|
|
|
+
|
|
|
+ :Ivariables:
|
|
|
+ id : int
|
|
|
+ id of the tile gid = TileSet.firstgid + Tile.id
|
|
|
+ images : list of :class:TileImage
|
|
|
+ list of TileImage, either its 'id' or 'image data' will be set
|
|
|
+ properties : dict of name:value
|
|
|
+ the propertis set in the editor, name-value pairs
|
|
|
+ """
|
|
|
+
|
|
|
+# [20:22] DR0ID_: to sum up: there are two use cases,
|
|
|
+# if the tile element has a child element 'image' then tile is
|
|
|
+# standalone with its own id and
|
|
|
+# the other case where a tileset is present then it
|
|
|
+# referes to the image with that id in the tileset
|
|
|
+
|
|
|
+ def __init__(self):
|
|
|
+ self.id = 0
|
|
|
+ self.images = [] # uses TileImage but either only id will be set or image data
|
|
|
+ self.properties = {} # {name: value}
|
|
|
+
|
|
|
+# -----------------------------------------------------------------------------
|
|
|
+
|
|
|
+class TileLayer(object):
|
|
|
+ """
|
|
|
+ A layer of the world.
|
|
|
+
|
|
|
+ :Ivariables:
|
|
|
+ x : int
|
|
|
+ position of layer in the world in number of tiles (not pixels)
|
|
|
+ y : int
|
|
|
+ position of layer in the world in number of tiles (not pixels)
|
|
|
+ width : int
|
|
|
+ number of tiles in x direction
|
|
|
+ height : int
|
|
|
+ number of tiles in y direction
|
|
|
+ pixel_width : int
|
|
|
+ width of layer in pixels
|
|
|
+ pixel_height : int
|
|
|
+ height of layer in pixels
|
|
|
+ name : string
|
|
|
+ name of this layer
|
|
|
+ opacity : float
|
|
|
+ float from 0 (full transparent) to 1.0 (opaque)
|
|
|
+ decoded_content : list
|
|
|
+ list of graphics id going through the map::
|
|
|
+
|
|
|
+ e.g [1, 1, 1, ]
|
|
|
+ where decoded_content[0] is (0,0)
|
|
|
+ decoded_content[1] is (1,0)
|
|
|
+ ...
|
|
|
+ decoded_content[w] is (width,0)
|
|
|
+ decoded_content[w+1] is (0,1)
|
|
|
+ ...
|
|
|
+ decoded_content[w * h] is (width,height)
|
|
|
+
|
|
|
+ usage: graphics id = decoded_content[tile_x + tile_y * width]
|
|
|
+ content2D : list
|
|
|
+ list of list, usage: graphics id = content2D[x][y]
|
|
|
+
|
|
|
+ """
|
|
|
+
|
|
|
+ def __init__(self):
|
|
|
+ self.width = 0
|
|
|
+ self.height = 0
|
|
|
+ self.x = 0
|
|
|
+ self.y = 0
|
|
|
+ self.pixel_width = 0
|
|
|
+ self.pixel_height = 0
|
|
|
+ self.name = None
|
|
|
+ self.opacity = -1
|
|
|
+ self.encoding = None
|
|
|
+ self.compression = None
|
|
|
+ self.encoded_content = None
|
|
|
+ self.decoded_content = []
|
|
|
+ self.visible = True
|
|
|
+ self.properties = {} # {name: value}
|
|
|
+ self.content2D = None
|
|
|
+ self.is_object_group = False # ISSUE 9
|
|
|
+
|
|
|
+
|
|
|
+ def decode(self):
|
|
|
+ """
|
|
|
+ Converts the contents in a list of integers which are the gid of the
|
|
|
+ used tiles. If necessairy it decodes and uncompresses the contents.
|
|
|
+ """
|
|
|
+ self.decoded_content = []
|
|
|
+ if self.encoded_content:
|
|
|
+ content = self.encoded_content
|
|
|
+ if self.encoding:
|
|
|
+ if self.encoding.lower() == 'base64':
|
|
|
+ content = decode_base64(content)
|
|
|
+ elif self.encoding.lower() == 'csv':
|
|
|
+ list_of_lines = content.split()
|
|
|
+ for line in list_of_lines:
|
|
|
+ self.decoded_content.extend(line.split(','))
|
|
|
+ self.decoded_content = list(map(int, \
|
|
|
+ [val for val in self.decoded_content if val]))
|
|
|
+ content = ""
|
|
|
+ else:
|
|
|
+ raise Exception('unknown data encoding %s' % \
|
|
|
+ (self.encoding))
|
|
|
+ else:
|
|
|
+ # in the case of xml the encoded_content already contains a
|
|
|
+ # list of integers
|
|
|
+ self.decoded_content = list(map(int, self.encoded_content))
|
|
|
+ content = ""
|
|
|
+ if self.compression:
|
|
|
+ if self.compression == 'gzip':
|
|
|
+ content = decompress_gzip(content)
|
|
|
+ elif self.compression == 'zlib':
|
|
|
+ content = decompress_zlib(content)
|
|
|
+ else:
|
|
|
+ raise Exception('unknown data compression %s' % \
|
|
|
+ (self.compression))
|
|
|
+ else:
|
|
|
+ raise Exception('no encoded content to decode')
|
|
|
+
|
|
|
+ struc = struct.Struct("<" + "I" * self.width)
|
|
|
+ struc_unpack_from = struc.unpack_from
|
|
|
+ self_decoded_content_extend = self.decoded_content.extend
|
|
|
+ for idx in range(0, len(content), 4 * self.width):
|
|
|
+ val = struc_unpack_from(content, idx)
|
|
|
+ self_decoded_content_extend(val)
|
|
|
+
|
|
|
+ arr = array.array('I')
|
|
|
+ arr.fromlist(self.decoded_content)
|
|
|
+ self.decoded_content = arr
|
|
|
+
|
|
|
+ # TODO: generate property grid here??
|
|
|
+
|
|
|
+ self._gen_2D()
|
|
|
+
|
|
|
+ def _gen_2D(self):
|
|
|
+ self.content2D = []
|
|
|
+
|
|
|
+ # generate the needed lists and fill them
|
|
|
+ for xpos in range(self.width):
|
|
|
+ self.content2D.append(array.array('I'))
|
|
|
+ for ypos in range(self.height):
|
|
|
+ self.content2D[xpos].append( \
|
|
|
+ self.decoded_content[xpos + ypos * self.width])
|
|
|
+
|
|
|
+ def pretty_print(self):
|
|
|
+ num = 0
|
|
|
+ for y in range(int(self.height)):
|
|
|
+ output = ""
|
|
|
+ for x in range(int(self.width)):
|
|
|
+ output += str(self.decoded_content[num])
|
|
|
+ num += 1
|
|
|
+ print(output)
|
|
|
+
|
|
|
+ def convert(self):
|
|
|
+ self.opacity = float(self.opacity)
|
|
|
+ self.x = int(self.x)
|
|
|
+ self.y = int(self.y)
|
|
|
+ self.width = int(self.width)
|
|
|
+ self.height = int(self.height)
|
|
|
+ self.pixel_width = self.width * self.tilewidth
|
|
|
+ self.pixel_height = self.height * self.tileheight
|
|
|
+ self.visible = bool(int(self.visible))
|
|
|
+
|
|
|
+ # def get_visible_tile_range(self, xmin, ymin, xmax, ymax):
|
|
|
+ # tile_w = self.pixel_width / self.width
|
|
|
+ # tile_h = self.pixel_height / self.height
|
|
|
+ # left = int(round(float(xmin) / tile_w)) - 1
|
|
|
+ # right = int(round(float(xmax) / tile_w)) + 2
|
|
|
+ # top = int(round(float(ymin) / tile_h)) - 1
|
|
|
+ # bottom = int(round(float(ymax) / tile_h)) + 2
|
|
|
+ # return (left, top, left - right, top - bottom)
|
|
|
+
|
|
|
+ # def get_tiles(self, xmin, ymin, xmax, ymax):
|
|
|
+ # tiles = []
|
|
|
+ # if self.visible:
|
|
|
+ # for ypos in range(ymin, ymax):
|
|
|
+ # for xpos in range(xmin, xmax):
|
|
|
+ # try:
|
|
|
+ # img_idx = self.content2D[xpos][ypos]
|
|
|
+ # if img_idx:
|
|
|
+ # tiles.append((xpos, ypos, img_idx))
|
|
|
+ # except IndexError:
|
|
|
+ # pass
|
|
|
+ # return tiles
|
|
|
+
|
|
|
+# -----------------------------------------------------------------------------
|
|
|
+
|
|
|
+
|
|
|
+class MapObjectGroupLayer(object):
|
|
|
+ """
|
|
|
+ Group of objects on the map.
|
|
|
+
|
|
|
+ :Ivariables:
|
|
|
+ x : int
|
|
|
+ the x position
|
|
|
+ y : int
|
|
|
+ the y position
|
|
|
+ width : int
|
|
|
+ width of the bounding box (usually 0, so no use)
|
|
|
+ height : int
|
|
|
+ height of the bounding box (usually 0, so no use)
|
|
|
+ name : string
|
|
|
+ name of the group
|
|
|
+ objects : list
|
|
|
+ list of the map objects
|
|
|
+
|
|
|
+ """
|
|
|
+
|
|
|
+ def __init__(self):
|
|
|
+ self.width = 0
|
|
|
+ self.height = 0
|
|
|
+ self.name = None
|
|
|
+ self.objects = []
|
|
|
+ self.x = 0
|
|
|
+ self.y = 0
|
|
|
+ self.visible = True
|
|
|
+ self.properties = {} # {name: value}
|
|
|
+ self.is_object_group = True # ISSUE 9
|
|
|
+
|
|
|
+ def convert(self):
|
|
|
+ self.x = int(self.x)
|
|
|
+ self.y = int(self.y)
|
|
|
+ self.width = int(self.width)
|
|
|
+ self.height = int(self.height)
|
|
|
+ for map_obj in self.objects:
|
|
|
+ map_obj.x = int(float(map_obj.x))
|
|
|
+ map_obj.y = int(float(map_obj.y))
|
|
|
+ map_obj.width = int(float(map_obj.width))
|
|
|
+ map_obj.height = int(float(map_obj.height))
|
|
|
+
|
|
|
+# -----------------------------------------------------------------------------
|
|
|
+
|
|
|
+class MapObject(object):
|
|
|
+ """
|
|
|
+ A single object on the map.
|
|
|
+
|
|
|
+ :Ivariables:
|
|
|
+ x : int
|
|
|
+ x position relative to group x position
|
|
|
+ y : int
|
|
|
+ y position relative to group y position
|
|
|
+ width : int
|
|
|
+ width of this object
|
|
|
+ height : int
|
|
|
+ height of this object
|
|
|
+ type : string
|
|
|
+ the type of this object
|
|
|
+ image_source : string
|
|
|
+ source path of the image for this object
|
|
|
+ image : :class:TileImage
|
|
|
+ after loading this is the pygame surface containing the image
|
|
|
+ """
|
|
|
+ def __init__(self):
|
|
|
+ self.name = None
|
|
|
+ self.x = 0
|
|
|
+ self.y = 0
|
|
|
+ self.width = 0
|
|
|
+ self.height = 0
|
|
|
+ self.type = None
|
|
|
+ self.image_source = None
|
|
|
+ self.image = None
|
|
|
+ self.properties = {} # {name: value}
|
|
|
+
|
|
|
+# -----------------------------------------------------------------------------
|
|
|
+def decode_base64(in_str):
|
|
|
+ """
|
|
|
+ Decodes a base64 string and returns it.
|
|
|
+
|
|
|
+ :Parameters:
|
|
|
+ in_str : string
|
|
|
+ base64 encoded string
|
|
|
+
|
|
|
+ :returns: decoded string
|
|
|
+ """
|
|
|
+ import base64
|
|
|
+ return base64.decodestring(in_str.encode('latin-1'))
|
|
|
+
|
|
|
+# -----------------------------------------------------------------------------
|
|
|
+def decompress_gzip(in_str):
|
|
|
+ """
|
|
|
+ Uncompresses a gzip string and returns it.
|
|
|
+
|
|
|
+ :Parameters:
|
|
|
+ in_str : string
|
|
|
+ gzip compressed string
|
|
|
+
|
|
|
+ :returns: uncompressed string
|
|
|
+ """
|
|
|
+ import gzip
|
|
|
+
|
|
|
+ if sys.version_info > (2, ):
|
|
|
+ from io import BytesIO
|
|
|
+ copmressed_stream = BytesIO(in_str)
|
|
|
+ else:
|
|
|
+ # gzip can only handle file object therefore using StringIO
|
|
|
+ copmressed_stream = StringIO(in_str.decode("latin-1"))
|
|
|
+ gzipper = gzip.GzipFile(fileobj=copmressed_stream)
|
|
|
+ content = gzipper.read()
|
|
|
+ gzipper.close()
|
|
|
+ return content
|
|
|
+
|
|
|
+# -----------------------------------------------------------------------------
|
|
|
+def decompress_zlib(in_str):
|
|
|
+ """
|
|
|
+ Uncompresses a zlib string and returns it.
|
|
|
+
|
|
|
+ :Parameters:
|
|
|
+ in_str : string
|
|
|
+ zlib compressed string
|
|
|
+
|
|
|
+ :returns: uncompressed string
|
|
|
+ """
|
|
|
+ import zlib
|
|
|
+ content = zlib.decompress(in_str)
|
|
|
+ return content
|
|
|
+# -----------------------------------------------------------------------------
|
|
|
+def printer(obj, ident=''):
|
|
|
+ """
|
|
|
+ Helper function, prints a hirarchy of objects.
|
|
|
+ """
|
|
|
+ import inspect
|
|
|
+ print(ident + obj.__class__.__name__.upper())
|
|
|
+ ident += ' '
|
|
|
+ lists = []
|
|
|
+ for name in dir(obj):
|
|
|
+ elem = getattr(obj, name)
|
|
|
+ if isinstance(elem, list) and name != 'decoded_content':
|
|
|
+ lists.append(elem)
|
|
|
+ elif not inspect.ismethod(elem):
|
|
|
+ if not name.startswith('__'):
|
|
|
+ if name == 'data' and elem:
|
|
|
+ print(ident + 'data = ')
|
|
|
+ printer(elem, ident + ' ')
|
|
|
+ else:
|
|
|
+ print(ident + '%s\t= %s' % (name, getattr(obj, name)))
|
|
|
+ for objt_list in lists:
|
|
|
+ for _obj in objt_list:
|
|
|
+ printer(_obj, ident + ' ')
|
|
|
+
|
|
|
+# -----------------------------------------------------------------------------
|
|
|
+
|
|
|
+class VersionError(Exception): pass
|
|
|
+
|
|
|
+# -----------------------------------------------------------------------------
|
|
|
+class TileMapParser(object):
|
|
|
+ """
|
|
|
+ Allows to parse and decode map files for 'Tiled', a open source map editor
|
|
|
+ written in java. It can be found here: http://mapeditor.org/
|
|
|
+ """
|
|
|
+
|
|
|
+ def _build_tile_set(self, tile_set_node, world_map):
|
|
|
+ tile_set = TileSet()
|
|
|
+ self._set_attributes(tile_set_node, tile_set)
|
|
|
+ if hasattr(tile_set, "source"):
|
|
|
+ tile_set = self._parse_tsx(tile_set.source, tile_set, world_map)
|
|
|
+ else:
|
|
|
+ tile_set = self._get_tile_set(tile_set_node, tile_set, \
|
|
|
+ self.map_file_name)
|
|
|
+ world_map.tile_sets.append(tile_set)
|
|
|
+
|
|
|
+ def _parse_tsx(self, file_name, tile_set, world_map):
|
|
|
+ # ISSUE 5: the *.tsx file is probably relative to the *.tmx file
|
|
|
+ if not os.path.isabs(file_name):
|
|
|
+ # print "map file name", self.map_file_name
|
|
|
+ file_name = self._get_abs_path(self.map_file_name, file_name)
|
|
|
+ # print "tsx filename: ", file_name
|
|
|
+ # would be more elegant to use "with open(file_name, "rb") as file:"
|
|
|
+ # but that is python 2.6
|
|
|
+ file = None
|
|
|
+ try:
|
|
|
+ file = open(file_name, "rb")
|
|
|
+ dom = minidom.parseString(file.read())
|
|
|
+ finally:
|
|
|
+ if file:
|
|
|
+ file.close()
|
|
|
+ for node in self._get_nodes(dom.childNodes, 'tileset'):
|
|
|
+ tile_set = self._get_tile_set(node, tile_set, file_name)
|
|
|
+ break
|
|
|
+ return tile_set
|
|
|
+
|
|
|
+ def _get_tile_set(self, tile_set_node, tile_set, base_path):
|
|
|
+ for node in self._get_nodes(tile_set_node.childNodes, 'image'):
|
|
|
+ self._build_tile_set_image(node, tile_set, base_path)
|
|
|
+ for node in self._get_nodes(tile_set_node.childNodes, 'tile'):
|
|
|
+ self._build_tile_set_tile(node, tile_set)
|
|
|
+ self._set_attributes(tile_set_node, tile_set)
|
|
|
+ return tile_set
|
|
|
+
|
|
|
+ def _build_tile_set_image(self, image_node, tile_set, base_path):
|
|
|
+ image = TileImage()
|
|
|
+ self._set_attributes(image_node, image)
|
|
|
+ # id of TileImage has to be set! -> Tile.TileImage will only have id set
|
|
|
+ for node in self._get_nodes(image_node.childNodes, 'data'):
|
|
|
+ self._set_attributes(node, image)
|
|
|
+ image.content = node.childNodes[0].nodeValue
|
|
|
+ image.source = self._get_abs_path(base_path, image.source) # ISSUE 5
|
|
|
+ tile_set.images.append(image)
|
|
|
+
|
|
|
+ def _get_abs_path(self, base, relative):
|
|
|
+ if os.path.isabs(relative):
|
|
|
+ return relative
|
|
|
+ if os.path.isfile(base):
|
|
|
+ base = os.path.dirname(base)
|
|
|
+ return os.path.abspath(os.path.join(base, relative))
|
|
|
+
|
|
|
+ def _build_tile_set_tile(self, tile_set_node, tile_set):
|
|
|
+ tile = Tile()
|
|
|
+ self._set_attributes(tile_set_node, tile)
|
|
|
+ for node in self._get_nodes(tile_set_node.childNodes, 'image'):
|
|
|
+ self._build_tile_set_tile_image(node, tile)
|
|
|
+ tile_set.tiles.append(tile)
|
|
|
+
|
|
|
+ def _build_tile_set_tile_image(self, tile_node, tile):
|
|
|
+ tile_image = TileImage()
|
|
|
+ self._set_attributes(tile_node, tile_image)
|
|
|
+ for node in self._get_nodes(tile_node.childNodes, 'data'):
|
|
|
+ self._set_attributes(node, tile_image)
|
|
|
+ tile_image.content = node.childNodes[0].nodeValue
|
|
|
+ tile.images.append(tile_image)
|
|
|
+
|
|
|
+ def _build_layer(self, layer_node, world_map):
|
|
|
+ layer = TileLayer()
|
|
|
+ self._set_attributes(layer_node, layer)
|
|
|
+ for node in self._get_nodes(layer_node.childNodes, 'data'):
|
|
|
+ self._set_attributes(node, layer)
|
|
|
+ if layer.encoding:
|
|
|
+ layer.encoded_content = node.lastChild.nodeValue
|
|
|
+ else:
|
|
|
+ #print 'has childnodes', node.hasChildNodes()
|
|
|
+ layer.encoded_content = []
|
|
|
+ for child in node.childNodes:
|
|
|
+ if child.nodeType == Node.ELEMENT_NODE and \
|
|
|
+ child.nodeName == "tile":
|
|
|
+ val = child.attributes["gid"].nodeValue
|
|
|
+ #print child, val
|
|
|
+ layer.encoded_content.append(val)
|
|
|
+ world_map.layers.append(layer)
|
|
|
+
|
|
|
+ def _build_world_map(self, world_node):
|
|
|
+ world_map = TileMap()
|
|
|
+ self._set_attributes(world_node, world_map)
|
|
|
+ if world_map.version != "1.0":
|
|
|
+ raise VersionError('this parser was made for maps of version 1.0, found version %s' % world_map.version)
|
|
|
+ for node in self._get_nodes(world_node.childNodes, 'tileset'):
|
|
|
+ self._build_tile_set(node, world_map)
|
|
|
+ for node in self._get_nodes(world_node.childNodes, 'layer'):
|
|
|
+ self._build_layer(node, world_map)
|
|
|
+ for node in self._get_nodes(world_node.childNodes, 'objectgroup'):
|
|
|
+ self._build_object_groups(node, world_map)
|
|
|
+ return world_map
|
|
|
+
|
|
|
+ def _build_object_groups(self, object_group_node, world_map):
|
|
|
+ object_group = MapObjectGroupLayer()
|
|
|
+ self._set_attributes(object_group_node, object_group)
|
|
|
+ for node in self._get_nodes(object_group_node.childNodes, 'object'):
|
|
|
+ tiled_object = MapObject()
|
|
|
+ self._set_attributes(node, tiled_object)
|
|
|
+ for p in self._get_nodes(node.childNodes, 'polyline'):
|
|
|
+ def xy(s):
|
|
|
+ (x, y) = [int(c) for c in s.split(",")]
|
|
|
+ return (int(tiled_object.x) + x, int(tiled_object.y) + y)
|
|
|
+ tiled_object.polyline = \
|
|
|
+ [xy(s) for s in p.attributes['points'].nodeValue.split()]
|
|
|
+ for img_node in self._get_nodes(node.childNodes, 'image'):
|
|
|
+ tiled_object.image_source = \
|
|
|
+ img_node.attributes['source'].nodeValue
|
|
|
+ object_group.objects.append(tiled_object)
|
|
|
+ # ISSUE 9
|
|
|
+ world_map.layers.append(object_group)
|
|
|
+
|
|
|
+ # -- helpers -- #
|
|
|
+ def _get_nodes(self, nodes, name):
|
|
|
+ for node in nodes:
|
|
|
+ if node.nodeType == Node.ELEMENT_NODE and node.nodeName == name:
|
|
|
+ yield node
|
|
|
+
|
|
|
+ def _set_attributes(self, node, obj):
|
|
|
+ attrs = node.attributes
|
|
|
+ for attr_name in list(attrs.keys()):
|
|
|
+ setattr(obj, attr_name, attrs.get(attr_name).nodeValue)
|
|
|
+ self._get_properties(node, obj)
|
|
|
+
|
|
|
+ def _get_properties(self, node, obj):
|
|
|
+ props = {}
|
|
|
+ for properties_node in self._get_nodes(node.childNodes, 'properties'):
|
|
|
+ for property_node in self._get_nodes(properties_node.childNodes, 'property'):
|
|
|
+ try:
|
|
|
+ props[property_node.attributes['name'].nodeValue] = \
|
|
|
+ property_node.attributes['value'].nodeValue
|
|
|
+ except KeyError:
|
|
|
+ props[property_node.attributes['name'].nodeValue] = \
|
|
|
+ property_node.lastChild.nodeValue
|
|
|
+ obj.properties.update(props)
|
|
|
+
|
|
|
+
|
|
|
+ # -- parsers -- #
|
|
|
+ def parse(self, file_name):
|
|
|
+ """
|
|
|
+ Parses the given map. Does no decoding nor loading of the data.
|
|
|
+ :return: instance of TileMap
|
|
|
+ """
|
|
|
+ # would be more elegant to use
|
|
|
+ # "with open(file_name, "rb") as tmx_file:" but that is python 2.6
|
|
|
+ self.map_file_name = os.path.abspath(file_name)
|
|
|
+ tmx_file = None
|
|
|
+ try:
|
|
|
+ tmx_file = open(self.map_file_name, "rb")
|
|
|
+ dom = minidom.parseString(tmx_file.read())
|
|
|
+ finally:
|
|
|
+ if tmx_file:
|
|
|
+ tmx_file.close()
|
|
|
+ for node in self._get_nodes(dom.childNodes, 'map'):
|
|
|
+ world_map = self._build_world_map(node)
|
|
|
+ break
|
|
|
+ world_map.map_file_name = self.map_file_name
|
|
|
+ world_map.convert()
|
|
|
+ return world_map
|
|
|
+
|
|
|
+ def parse_decode(self, file_name):
|
|
|
+ """
|
|
|
+ Parses the map but additionally decodes the data.
|
|
|
+ :return: instance of TileMap
|
|
|
+ """
|
|
|
+ world_map = self.parse(file_name)
|
|
|
+ world_map.decode()
|
|
|
+ return world_map
|
|
|
+
|
|
|
+
|
|
|
+# -----------------------------------------------------------------------------
|
|
|
+
|
|
|
+class AbstractResourceLoader(object):
|
|
|
+ """
|
|
|
+ Abstract base class for the resource loader.
|
|
|
+
|
|
|
+ """
|
|
|
+
|
|
|
+ FLIP_X = 1 << 31
|
|
|
+ FLIP_Y = 1 << 30
|
|
|
+
|
|
|
+ def __init__(self):
|
|
|
+ self.indexed_tiles = {} # {gid: (offsetx, offsety, image}
|
|
|
+ self.world_map = None
|
|
|
+ self._img_cache = {}
|
|
|
+
|
|
|
+ def _load_image(self, filename, colorkey=None): # -> image
|
|
|
+ """
|
|
|
+ Load a single image.
|
|
|
+
|
|
|
+ :Parameters:
|
|
|
+ filename : string
|
|
|
+ Path to the file to be loaded.
|
|
|
+ colorkey : tuple
|
|
|
+ The (r, g, b) color that should be used as colorkey
|
|
|
+ (or magic color).
|
|
|
+ Default: None
|
|
|
+
|
|
|
+ :rtype: image
|
|
|
+
|
|
|
+ """
|
|
|
+ raise NotImplementedError('This should be implemented in a inherited class')
|
|
|
+
|
|
|
+ def _load_image_file_like(self, file_like_obj, colorkey=None): # -> image
|
|
|
+ """
|
|
|
+ Load a image from a file like object.
|
|
|
+
|
|
|
+ :Parameters:
|
|
|
+ file_like_obj : file
|
|
|
+ This is the file like object to load the image from.
|
|
|
+ colorkey : tuple
|
|
|
+ The (r, g, b) color that should be used as colorkey
|
|
|
+ (or magic color).
|
|
|
+ Default: None
|
|
|
+
|
|
|
+ :rtype: image
|
|
|
+ """
|
|
|
+ raise NotImplementedError('This should be implemented in a inherited class')
|
|
|
+
|
|
|
+ def _load_image_parts(self, filename, margin, spacing, tilewidth, tileheight, colorkey=None): #-> [images]
|
|
|
+ """
|
|
|
+ Load different tile images from one source image.
|
|
|
+
|
|
|
+ :Parameters:
|
|
|
+ filename : string
|
|
|
+ Path to image to be loaded.
|
|
|
+ margin : int
|
|
|
+ The margin around the image.
|
|
|
+ spacing : int
|
|
|
+ The space between the tile images.
|
|
|
+ tilewidth : int
|
|
|
+ The width of a single tile.
|
|
|
+ tileheight : int
|
|
|
+ The height of a single tile.
|
|
|
+ colorkey : tuple
|
|
|
+ The (r, g, b) color that should be used as colorkey
|
|
|
+ (or magic color).
|
|
|
+ Default: None
|
|
|
+
|
|
|
+ Luckily that iteration is so easy in python::
|
|
|
+
|
|
|
+ ...
|
|
|
+ w, h = image_size
|
|
|
+ for y in xrange(margin, h, tileheight + spacing):
|
|
|
+ for x in xrange(margin, w, tilewidth + spacing):
|
|
|
+ ...
|
|
|
+
|
|
|
+ :rtype: a list of images
|
|
|
+ """
|
|
|
+ raise NotImplementedError('This should be implemented in a inherited class')
|
|
|
+
|
|
|
+ def load(self, tile_map):
|
|
|
+ """
|
|
|
+ """
|
|
|
+ self.world_map = tile_map
|
|
|
+ for tile_set in tile_map.tile_sets:
|
|
|
+ # do images first, because tiles could reference it
|
|
|
+ for img in tile_set.images:
|
|
|
+ if img.source:
|
|
|
+ self._load_image_from_source(tile_map, tile_set, img)
|
|
|
+ else:
|
|
|
+ tile_set.indexed_images[img.id] = self._load_tile_image(img)
|
|
|
+ # tiles
|
|
|
+ for tile in tile_set.tiles:
|
|
|
+ for img in tile.images:
|
|
|
+ if not img.content and not img.source:
|
|
|
+ # only image id set
|
|
|
+ indexed_img = tile_set.indexed_images[img.id]
|
|
|
+ self.indexed_tiles[int(tile_set.firstgid) + int(tile.id)] = (0, 0, indexed_img)
|
|
|
+ else:
|
|
|
+ if img.source:
|
|
|
+ self._load_image_from_source(tile_map, tile_set, img)
|
|
|
+ else:
|
|
|
+ indexed_img = self._load_tile_image(img)
|
|
|
+ self.indexed_tiles[int(tile_set.firstgid) + int(tile.id)] = (0, 0, indexed_img)
|
|
|
+
|
|
|
+ def _load_image_from_source(self, tile_map, tile_set, a_tile_image):
|
|
|
+ # relative path to file
|
|
|
+ img_path = os.path.join(os.path.dirname(tile_map.map_file_name), \
|
|
|
+ a_tile_image.source)
|
|
|
+ tile_width = int(tile_map.tilewidth)
|
|
|
+ tile_height = int(tile_map.tileheight)
|
|
|
+ if tile_set.tileheight:
|
|
|
+ tile_width = int(tile_set.tilewidth)
|
|
|
+ if tile_set.tilewidth:
|
|
|
+ tile_height = int(tile_set.tileheight)
|
|
|
+ offsetx = 0
|
|
|
+ offsety = 0
|
|
|
+ # the offset is used for pygame because the origin is topleft in pygame
|
|
|
+ if tile_height > tile_map.tileheight:
|
|
|
+ offsety = tile_height - tile_map.tileheight
|
|
|
+ idx = 0
|
|
|
+ for image in self._load_image_parts(img_path, \
|
|
|
+ tile_set.margin, tile_set.spacing, \
|
|
|
+ tile_width, tile_height, a_tile_image.trans):
|
|
|
+ self.indexed_tiles[int(tile_set.firstgid) + idx] = \
|
|
|
+ (offsetx, -offsety, image)
|
|
|
+ idx += 1
|
|
|
+
|
|
|
+ def _load_tile_image(self, a_tile_image):
|
|
|
+ img_str = a_tile_image.content
|
|
|
+ if a_tile_image.encoding:
|
|
|
+ if a_tile_image.encoding == 'base64':
|
|
|
+ img_str = decode_base64(a_tile_image.content)
|
|
|
+ else:
|
|
|
+ raise Exception('unknown image encoding %s' % a_tile_image.encoding)
|
|
|
+ sio = StringIO(img_str)
|
|
|
+ new_image = self._load_image_file_like(sio, a_tile_image.trans)
|
|
|
+ return new_image
|
|
|
+
|
|
|
+# -----------------------------------------------------------------------------
|
|
|
+
|