diff options
author | Livio Recchia <recchialivio@libero.it> | 2020-02-10 23:06:34 +0100 |
---|---|---|
committer | Livio Recchia <recchialivio@libero.it> | 2020-02-10 23:06:34 +0100 |
commit | 9a13903a2f7d3a65fdf15a65fb59cccd622e2066 (patch) | |
tree | 9403b7dff39eb5e5d7fa0f79efb69b496add4c4b /external/pytmx | |
parent | 11cc316b74d5f3f283413a33e7693b314741aa4a (diff) | |
download | manachat-9a13903a2f7d3a65fdf15a65fb59cccd622e2066.tar.gz manachat-9a13903a2f7d3a65fdf15a65fb59cccd622e2066.tar.bz2 manachat-9a13903a2f7d3a65fdf15a65fb59cccd622e2066.tar.xz manachat-9a13903a2f7d3a65fdf15a65fb59cccd622e2066.zip |
Initial commit
Diffstat (limited to 'external/pytmx')
-rw-r--r-- | external/pytmx/LICENSE | 165 | ||||
-rw-r--r-- | external/pytmx/__init__.py | 19 | ||||
-rw-r--r-- | external/pytmx/pytmx.py | 1115 | ||||
-rw-r--r-- | external/pytmx/readme.md | 586 | ||||
-rw-r--r-- | external/pytmx/util_pygame.py | 269 | ||||
-rw-r--r-- | external/pytmx/util_pyglet.py | 59 | ||||
-rw-r--r-- | external/pytmx/util_pysdl2.py | 66 |
7 files changed, 2279 insertions, 0 deletions
diff --git a/external/pytmx/LICENSE b/external/pytmx/LICENSE new file mode 100644 index 0000000..65c5ca8 --- /dev/null +++ b/external/pytmx/LICENSE @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/> + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/external/pytmx/__init__.py b/external/pytmx/__init__.py new file mode 100644 index 0000000..46c8707 --- /dev/null +++ b/external/pytmx/__init__.py @@ -0,0 +1,19 @@ +import logging + +logger = logging.getLogger(__name__) +ch = logging.StreamHandler() +ch.setLevel(logging.INFO) +logger.addHandler(ch) +logger.setLevel(logging.INFO) + +from .pytmx import * +try: + from pytmx.util_pygame import load_pygame +except ImportError: + logger.debug('cannot import pygame tools') + + +__version__ = (3, 20, 15) +__author__ = 'bitcraft' +__author_email__ = 'leif.theden@gmail.com' +__description__ = 'Map loader for TMX Files - Python 2 and 3' diff --git a/external/pytmx/pytmx.py b/external/pytmx/pytmx.py new file mode 100644 index 0000000..61120a0 --- /dev/null +++ b/external/pytmx/pytmx.py @@ -0,0 +1,1115 @@ +import logging +import six +import os +from itertools import chain, product +from collections import defaultdict, namedtuple +from xml.etree import ElementTree +from six.moves import zip, map +from operator import attrgetter + +logger = logging.getLogger(__name__) +ch = logging.StreamHandler() +ch.setLevel(logging.INFO) +logger.addHandler(ch) +logger.setLevel(logging.INFO) + +__all__ = ['TiledElement', + 'TiledMap', + 'TiledTileset', + 'TiledTileLayer', + 'TiledObject', + 'TiledObjectGroup', + 'TiledImageLayer', + 'TileFlags', + 'convert_to_bool', + 'parse_properties'] + +# internal flags +TRANS_FLIPX = 1 +TRANS_FLIPY = 2 +TRANS_ROT = 4 + +# Tiled gid flags +GID_TRANS_FLIPX = 1 << 31 +GID_TRANS_FLIPY = 1 << 30 +GID_TRANS_ROT = 1 << 29 + +# error message format strings go here +duplicate_name_fmt = 'Cannot set user {} property on {} "{}"; Tiled property already exists.' + +flag_names = ( + 'flipped_horizontally', + 'flipped_vertically', + 'flipped_diagonally',) + +TileFlags = namedtuple('TileFlags', flag_names) +AnimationFrame = namedtuple('AnimationFrame', ['gid', 'duration']) + + +def default_image_loader(filename, flags, **kwargs): + """ This default image loader just returns filename, rect, and any flags + """ + def load(rect=None, flags=None): + return filename, rect, flags + + return load + + +def decode_gid(raw_gid): + """ Decode a GID from TMX data + + as of 0.7.0 it determines if the tile should be flipped when rendered + as of 0.8.0 bit 30 determines if GID is rotated + + :param raw_gid: 32-bit number from TMX layer data + :return: gid, flags + """ + flags = TileFlags( + raw_gid & GID_TRANS_FLIPX == GID_TRANS_FLIPX, + raw_gid & GID_TRANS_FLIPY == GID_TRANS_FLIPY, + raw_gid & GID_TRANS_ROT == GID_TRANS_ROT) + gid = raw_gid & ~(GID_TRANS_FLIPX | GID_TRANS_FLIPY | GID_TRANS_ROT) + return gid, flags + + +def convert_to_bool(text): + """ Convert a few common variations of "true" and "false" to boolean + + :param text: string to test + :return: boolean + :raises: ValueError + """ + try: + return bool(int(text)) + except: + pass + + text = str(text).lower() + if text == "true": + return True + if text == "yes": + return True + if text == "false": + return False + if text == "no": + return False + + raise ValueError + +# used to change the unicode string returned from xml to +# proper python variable types. +types = defaultdict(lambda: str) +types.update({ + "version": float, + "orientation": str, + "width": float, + "height": float, + "tilewidth": int, + "tileheight": int, + "firstgid": int, + "source": str, + "name": str, + "spacing": int, + "margin": int, + "trans": str, + "id": int, + "opacity": float, + "visible": convert_to_bool, + "encoding": str, + "compression": str, + "gid": int, + "type": str, + "x": float, + "y": float, + "value": str, + "rotation": float, +}) + + +def parse_properties(node): + """ Parse a Tiled xml node and return a dict that represents a tiled "property" + + :param node: etree element + :return: dict + """ + d = dict() + for child in node.findall('properties'): + for subnode in child.findall('property'): + d[subnode.get('name')] = subnode.get('value') + return d + + +class TiledElement(object): + """ Base class for all pytmx types + """ + allow_duplicate_names = False + + def __init__(self): + self.properties = dict() + + @classmethod + def from_xml_string(cls, xml_string): + """Return a TileElement object from a xml string + + :param xml_string: string containing xml data + :rtype: TiledElement instance + """ + return cls().parse_xml(ElementTree.fromstring(xml_string)) + + def _cast_and_set_attributes_from_node_items(self, items): + for key, value in items: + casted_value = types[key](value) + setattr(self, key, casted_value) + + def _contains_invalid_property_name(self, items): + if self.allow_duplicate_names: + return False + + for k, v in items: + if hasattr(self, k): + msg = duplicate_name_fmt.format(k, self.__class__.__name__, self.name) + logger.error(msg) + return True + return False + + @staticmethod + def _log_property_error_message(self): + msg = 'Some name are reserved for {0} objects and cannot be used.' + logger.error(msg) + + def _set_properties(self, node): + """ Create dict containing Tiled object attributes from xml data + + read the xml attributes and tiled "properties" from a xml node and fill + in the values into the object's dictionary. Names will be checked to + make sure that they do not conflict with reserved names. + + :param node: etree element + :return: dict + """ + self._cast_and_set_attributes_from_node_items(node.items()) + properties = parse_properties(node) + if (not self.allow_duplicate_names and + self._contains_invalid_property_name(properties.items())): + self._log_property_error_message() + raise ValueError + + self.properties = properties + + def __getattr__(self, item): + try: + return self.properties[item] + except KeyError: + raise AttributeError + + def __repr__(self): + return '<{0}: "{1}">'.format(self.__class__.__name__, self.name) + + +class TiledMap(TiledElement): + """Contains the layers, objects, and images from a Tiled TMX map + + This class is meant to handle most of the work you need to do to use a map. + """ + + def __init__(self, filename=None, image_loader=default_image_loader, **kwargs): + """ Create new TiledMap + + :param filename: filename of tiled map to load + :param image_loader: function that will load images (see below) + :param optional_gids: load specific tile image GID, even if never used + :param invert_y: invert the y axis + :param load_all_tiles: load all tile images, even if never used + :param allow_duplicate_names: allow duplicates in objects' metatdata + + image_loader: + this must be a reference to a function that will accept a tuple: + (filename of image, bounding rect of tile in image, flags) + the function must return a reference to to the tile. + """ + TiledElement.__init__(self) + self.filename = filename + self.image_loader = image_loader + + # optional keyword arguments checked here + self.optional_gids = kwargs.get('optional_gids', set()) + self.load_all_tiles = kwargs.get('load_all', False) + self.invert_y = kwargs.get('invert_y', True) + + # allow duplicate names to be parsed and loaded + TiledElement.allow_duplicate_names = \ + kwargs.get('allow_duplicate_names', False) + + self.layers = list() # all layers in proper order + self.tilesets = list() # TiledTileset objects + self.tile_properties = dict() # tiles that have properties + self.layernames = dict() + + # only used tiles are actually loaded, so there will be a difference + # between the GIDs in the Tiled map data (tmx) and the data in this + # object and the layers. This dictionary keeps track of that. + self.gidmap = defaultdict(list) + self.imagemap = dict() # mapping of gid and trans flags to real gids + self.tiledgidmap = dict() # mapping of tiledgid to pytmx gid + self.maxgid = 1 + + # should be filled in by a loader function + self.images = list() + + # defaults from the TMX specification + self.version = 0.0 + self.orientation = None + self.width = 0 # width of map in tiles + self.height = 0 # height of map in tiles + self.tilewidth = 0 # width of a tile in pixels + self.tileheight = 0 # height of a tile in pixels + self.background_color = None + + # initialize the gid mapping + self.imagemap[(0, 0)] = 0 + + if filename: + self.parse_xml(ElementTree.parse(self.filename).getroot()) + + def __repr__(self): + return '<{0}: "{1}">'.format(self.__class__.__name__, self.filename) + + # iterate over layers and objects in map + def __iter__(self): + return chain(self.layers, self.objects) + + def _set_properties(self, node): + TiledElement._set_properties(self, node) + + # TODO: make class/layer-specific type casting + # layer height and width must be int, but TiledElement.set_properties() + # make a float by default, so recast as int here + self.height = int(self.height) + self.width = int(self.width) + + def parse_xml(self, node): + """ Parse a map from ElementTree xml node + + :param node: ElementTree xml node + :return: self + """ + self._set_properties(node) + self.background_color = node.get('backgroundcolor', + self.background_color) + + # *** do not change this load order! *** # + # *** gid mapping errors will occur if changed *** # + for subnode in node.findall('layer'): + self.add_layer(TiledTileLayer(self, subnode)) + + for subnode in node.findall('imagelayer'): + self.add_layer(TiledImageLayer(self, subnode)) + + for subnode in node.findall('objectgroup'): + self.add_layer(TiledObjectGroup(self, subnode)) + + for subnode in node.findall('tileset'): + self.add_tileset(TiledTileset(self, subnode)) + + # "tile objects", objects with a GID, have need to have their attributes + # set after the tileset is loaded, so this step must be performed last + # also, this step is performed for objects to load their tiles. + # height and width will automatically be set according to the tileset + # that the image is from + + # tiled stores the origin of GID objects by the lower right corner + # this is different for all other types, so i just adjust it here + # so all types loaded with pytmx are uniform. + + # iterate through tile objects and handle the image + for o in [o for o in self.objects if o.gid]: + + # gids might also have properties assigned to them + # in that case, assign the gid properties to the object as well + p = self.get_tile_properties_by_gid(o.gid) + if p: + o.properties.update(p) + + try: + tileset = self.get_tileset_from_gid(o.gid) + except ValueError: + msg = 'attempted to lookup invalid gid %s in object %s' + logger.error(msg, o.gid, o) + else: + if self.invert_y: + o.y -= tileset.tileheight + o.height = tileset.tileheight + o.width = tileset.tilewidth + + self.reload_images() + return self + + def reload_images(self): + """ Load the map images from disk + + This method will use the image loader passed in the constructor + to do the loading or will use a generic default, in which case no + images will be loaded. + + :return: None + """ + self.images = [None] * self.maxgid + + # iterate through tilesets to get source images + for ts in self.tilesets: + + # skip tilesets without a source + if ts.source is None: + continue + + path = os.path.join(os.path.dirname(self.filename), ts.source) + colorkey = getattr(ts, 'trans', None) + loader = self.image_loader(path, colorkey) + + p = product(range(ts.margin, + ts.height + ts.margin - ts.tileheight + 1, + ts.tileheight + ts.spacing), + range(ts.margin, + ts.width + ts.margin - ts.tilewidth + 1, + ts.tilewidth + ts.spacing)) + + # iterate through the tiles + for real_gid, (y, x) in enumerate(p, ts.firstgid): + rect = (x, y, ts.tilewidth, ts.tileheight) + gids = self.map_gid(real_gid) + + # gids is None if the tile is never used + # but give another chance to load the gid anyway + if gids is None: + if self.load_all_tiles or real_gid in self.optional_gids: + # TODO: handle flags? - might never be an issue, though + gids = [self.register_gid(real_gid, flags=0)] + + if gids: + # flags might rotate/flip the image, so let the loader + # handle that here + for gid, flags in gids: + self.images[gid] = loader(rect, flags) + + # load image layer images + for layer in (i for i in self.layers if isinstance(i, TiledImageLayer)): + source = getattr(layer, 'source', None) + if source: + colorkey = getattr(layer, 'trans', None) + real_gid = len(self.images) + gid = self.register_gid(real_gid) + layer.gid = gid + path = os.path.join(os.path.dirname(self.filename), source) + loader = self.image_loader(path, colorkey) + image = loader() + self.images.append(image) + + # load images in tiles. + # instead of making a new gid, replace the reference to the tile that + # was loaded from the tileset + for real_gid, props in self.tile_properties.items(): + source = props.get('source', None) + if source: + colorkey = props.get('trans', None) + path = os.path.join(os.path.dirname(self.filename), source) + loader = self.image_loader(path, colorkey) + image = loader() + self.images[real_gid] = image + + def get_tile_image(self, x, y, layer): + """ Return the tile image for this location + + :param x: x coordinate + :param y: y coordinate + :param layer: layer number + :rtype: surface if found, otherwise 0 + """ + try: + assert (x >= 0 and y >= 0) + except AssertionError: + raise ValueError + + try: + layer = self.layers[layer] + except IndexError: + raise ValueError + + assert (isinstance(layer, TiledTileLayer)) + + try: + gid = layer.data[y][x] + except (IndexError, ValueError): + raise ValueError + except TypeError: + msg = "Tiles must be specified in integers." + logger.debug(msg) + raise TypeError + + else: + return self.get_tile_image_by_gid(gid) + + def get_tile_image_by_gid(self, gid): + """ Return the tile image for this location + + :param gid: GID of image + :rtype: surface if found, otherwise ValueError + """ + try: + assert (int(gid) >= 0) + return self.images[gid] + except TypeError: + msg = "GIDs must be expressed as a number. Got: {0}" + logger.debug(msg.format(gid)) + raise TypeError + except (AssertionError, IndexError): + msg = "Coords: ({0},{1}) in layer {2} has invalid GID: {3}" + logger.debug(msg.format(gid)) + raise ValueError + + def get_tile_gid(self, x, y, layer): + """ Return the tile image GID for this location + + :param x: x coordinate + :param y: y coordinate + :param layer: layer number + :rtype: surface if found, otherwise ValueError + """ + try: + assert (x >= 0 and y >= 0 and layer >= 0) + except AssertionError: + raise ValueError + + try: + return self.layers[int(layer)].data[int(y)][int(x)] + except (IndexError, ValueError): + msg = "Coords: ({0},{1}) in layer {2} is invalid" + logger.debug(msg, (x, y, layer)) + raise ValueError + + def get_tile_properties(self, x, y, layer): + """ Return the tile image GID for this location + + :param x: x coordinate + :param y: y coordinate + :param layer: layer number + :rtype: python dict if found, otherwise None + """ + try: + assert (x >= 0 and y >= 0 and layer >= 0) + except AssertionError: + raise ValueError + + try: + gid = self.layers[int(layer)].data[int(y)][int(x)] + except (IndexError, ValueError): + msg = "Coords: ({0},{1}) in layer {2} is invalid." + logger.debug(msg.format(x, y, layer)) + raise Exception + + else: + try: + return self.tile_properties[gid] + except (IndexError, ValueError): + msg = "Coords: ({0},{1}) in layer {2} has invalid GID: {3}" + logger.debug(msg.format(x, y, layer, gid)) + raise Exception + except KeyError: + return None + + def get_tile_locations_by_gid(self, gid): + """ Search map for tile locations by the GID + + Note: Not a fast operation. Cache results if used often. + + :param gid: GID to be searched for + :rtype: generator of tile locations + """ + # use this func to make sure GID is valid + self.get_tile_image_by_gid(gid) + + p = product(range(self.width), + range(self.height), + range(len(self.layers))) + + return ((x, y, l) for (x, y, l) in p if + self.layers[l].data[y][x] == gid) + + def get_tile_properties_by_gid(self, gid): + """ Get the tile properties of a tile GID + + :param gid: GID + :rtype: python dict if found, otherwise None + """ + try: + return self.tile_properties[gid] + except KeyError: + return None + + def set_tile_properties(self, gid, properties): + """ Set the tile properties of a tile GID + + :param gid: GID + :param properties: python dict of properties for GID + """ + self.tile_properties[gid] = properties + + def get_tile_properties_by_layer(self, layer): + """ Get the tile properties of each GID in layer + + :param layer: layer number + :rtype: iterator of (gid, properties) tuples + """ + try: + assert (int(layer) >= 0) + layer = int(layer) + except (TypeError, AssertionError): + msg = "Layer must be a positive integer. Got {0} instead." + logger.debug(msg.format(type(layer))) + raise ValueError + + p = product(range(self.width), range(self.height)) + layergids = set(self.layers[layer].data[y][x] for x, y in p) + + for gid in layergids: + try: + yield gid, self.tile_properties[gid] + except KeyError: + continue + + def add_layer(self, layer): + """ Add a layer (TileTileLayer, TiledImageLayer, or TiledObjectGroup) + + :param layer: TileTileLayer, TiledImageLayer, TiledObjectGroup object + """ + assert ( + isinstance(layer, + (TiledTileLayer, TiledImageLayer, TiledObjectGroup))) + + self.layers.append(layer) + self.layernames[layer.name] = layer + + def add_tileset(self, tileset): + """ Add a tileset to the map + + :param tileset: TiledTileset + """ + assert (isinstance(tileset, TiledTileset)) + self.tilesets.append(tileset) + + def get_layer_by_name(self, name): + """Return a layer by name + + :param name: Name of layer. Case-sensitive. + :rtype: Layer object if found, otherwise ValueError + """ + try: + return self.layernames[name] + except KeyError: + msg = 'Layer "{0}" not found.' + logger.debug(msg.format(name)) + raise ValueError + + def get_object_by_name(self, name): + """Find an object + + :param name: Name of object. Case-sensitive. + :rtype: Object if found, otherwise ValueError + """ + for obj in self.objects: + if obj.name == name: + return obj + raise ValueError + + def get_tileset_from_gid(self, gid): + """ Return tileset that owns the gid + + Note: this is a slow operation, so if you are expecting to do this + often, it would be worthwhile to cache the results of this. + + :param gid: gid of tile image + :rtype: TiledTileset if found, otherwise ValueError + """ + try: + tiled_gid = self.tiledgidmap[gid] + except KeyError: + raise ValueError + + for tileset in sorted(self.tilesets, key=attrgetter('firstgid'), + reverse=True): + if tiled_gid >= tileset.firstgid: + return tileset + + raise ValueError + + @property + def objectgroups(self): + """Return iterator of all object groups + + :rtype: Iterator + """ + return (layer for layer in self.layers + if isinstance(layer, TiledObjectGroup)) + + @property + def objects(self): + """Return iterator of all the objects associated with this map + + :rtype: Iterator + """ + return chain(*self.objectgroups) + + @property + def visible_layers(self): + """Return iterator of Layer objects that are set 'visible' + + :rtype: Iterator + """ + return (l for l in self.layers if l.visible) + + @property + def visible_tile_layers(self): + """Return iterator of layer indexes that are set 'visible' + + :rtype: Iterator + """ + return (i for (i, l) in enumerate(self.layers) + if l.visible and isinstance(l, TiledTileLayer)) + + @property + def visible_object_groups(self): + """Return iterator of object group indexes that are set 'visible' + + :rtype: Iterator + """ + return (i for (i, l) in enumerate(self.layers) + if l.visible and isinstance(l, TiledObjectGroup)) + + def register_gid(self, tiled_gid, flags=None): + """ Used to manage the mapping of GIDs between the tmx and pytmx + + :param tiled_gid: GID that is found in TMX data + :rtype: GID that pytmx uses for the the GID passed + """ + if flags is None: + flags = TileFlags(0, 0, 0) + + if tiled_gid: + try: + return self.imagemap[(tiled_gid, flags)][0] + except KeyError: + gid = self.maxgid + self.maxgid += 1 + self.imagemap[(tiled_gid, flags)] = (gid, flags) + self.gidmap[tiled_gid].append((gid, flags)) + self.tiledgidmap[gid] = tiled_gid + return gid + + else: + return 0 + + def map_gid(self, tiled_gid): + """ Used to lookup a GID read from a TMX file's data + + :param tiled_gid: GID that is found in TMX data + :rtype: (GID, flags) for the the GID passed, None if not found + """ + try: + return self.gidmap[int(tiled_gid)] + except KeyError: + return None + except TypeError: + msg = "GIDs must be an integer" + logger.debug(msg) + raise TypeError + + +class TiledTileset(TiledElement): + """ Represents a Tiled Tileset + + External tilesets are supported. GID/ID's from Tiled are not guaranteed to + be the same after loaded. + """ + + def __init__(self, parent, node): + TiledElement.__init__(self) + self.parent = parent + self.offset = (0, 0) + + # defaults from the specification + self.firstgid = 0 + self.source = None + self.name = None + self.tilewidth = 0 + self.tileheight = 0 + self.spacing = 0 + self.margin = 0 + self.trans = None + self.width = 0 + self.height = 0 + + self.parse_xml(node) + + def parse_xml(self, node): + """ Parse a Tileset from ElementTree xml element + + A bit of mangling is done here so that tilesets that have external + TSX files appear the same as those that don't + + :param node: ElementTree element + :return: self + """ + import os + + # if true, then node references an external tileset + source = node.get('source', None) + if source: + if source[-4:].lower() == ".tsx": + + # external tilesets don't save this, store it for later + self.firstgid = int(node.get('firstgid')) + + # we need to mangle the path - tiled stores relative paths + dirname = os.path.dirname(self.parent.filename) + path = os.path.abspath(os.path.join(dirname, source)) + try: + node = ElementTree.parse(path).getroot() + except IOError: + msg = "Cannot load external tileset: {0}" + logger.error(msg.format(path)) + raise Exception + + else: + msg = "Found external tileset, but cannot handle type: {0}" + logger.error(msg.format(self.source)) + raise Exception + + self._set_properties(node) + + # since tile objects [probably] don't have a lot of metadata, + # we store it separately in the parent (a TiledMap instance) + register_gid = self.parent.register_gid + for child in node.getiterator('tile'): + tiled_gid = int(child.get("id")) + p = parse_properties(child) + + # handle tiles that have their own image + image = child.find('image') + if image is None: + p['width'] = self.tilewidth + p['height'] = self.tileheight + else: + p['source'] = image.get('source') + p['trans'] = image.get('trans', None) + p['width'] = image.get('width') + p['height'] = image.get('height') + + # handle tiles with animations + anim = child.find('animation') + frames = list() + p['frames'] = frames + if anim is not None: + for frame in anim.findall("frame"): + duration = int(frame.get('duration')) + gid = register_gid(int(frame.get('tileid')) + self.firstgid) + frames.append(AnimationFrame(gid, duration)) + + for gid, flags in self.parent.map_gid(tiled_gid + self.firstgid): + self.parent.set_tile_properties(gid, p) + + # handle the optional 'tileoffset' node + self.offset = node.find('tileoffset') + if self.offset is None: + self.offset = (0, 0) + else: + self.offset = (self.offset.get('x', 0), self.offset.get('y', 0)) + + image_node = node.find('image') + if image_node is not None: + self.source = image_node.get('source') + self.trans = image_node.get('trans', None) + self.width = int(image_node.get('width')) + self.height = int(image_node.get('height')) + + return self + + +class TiledTileLayer(TiledElement): + """ Represents a TileLayer + + To just get the tile images, use TiledTileLayer.tiles() + """ + + def __init__(self, parent, node): + TiledElement.__init__(self) + self.parent = parent + self.data = list() + + # defaults from the specification + self.name = None + self.opacity = 1.0 + self.visible = True + self.height = 0 + self.width = 0 + + self.parse_xml(node) + + def __iter__(self): + return self.iter_data() + + def iter_data(self): + """ Iterate over layer data + + Yields X, Y, GID tuples for each tile in the layer + + :return: Generator + """ + for y, x in product(range(self.height), range(self.width)): + yield x, y, self.data[y][x] + + def tiles(self): + """ Iterate over tile images of this layer + + This is an optimised generator function that returns + (tile_x, tile_y, tile_image) tuples, + + :rtype: Generator + :return: (x, y, image) tuples + """ + images = self.parent.images + data = self.data + for y, row in enumerate(data): + for x, gid in enumerate(row): + if gid: + yield x, y, images[gid] + + def _set_properties(self, node): + TiledElement._set_properties(self, node) + + # TODO: make class/layer-specific type casting + # layer height and width must be int, but TiledElement.set_properties() + # make a float by default, so recast as int here + self.height = int(self.height) + self.width = int(self.width) + + def parse_xml(self, node): + """ Parse a Tile Layer from ElementTree xml node + + :param node: ElementTree xml node + :return: self + """ + import struct + import array + + self._set_properties(node) + data = None + next_gid = None + data_node = node.find('data') + + encoding = data_node.get('encoding', None) + if encoding == 'base64': + from base64 import b64decode + + data = b64decode(data_node.text.strip()) + + elif encoding == 'csv': + next_gid = map(int, "".join( + line.strip() for line in data_node.text.strip() + ).split(",")) + + elif encoding: + msg = 'TMX encoding type: {0} is not supported.' + logger.error(msg.format(encoding)) + raise Exception + + compression = data_node.get('compression', None) + if compression == 'gzip': + import gzip + + with gzip.GzipFile(fileobj=six.BytesIO(data)) as fh: + data = fh.read() + + elif compression == 'zlib': + import zlib + + data = zlib.decompress(data) + + elif compression: + msg = 'TMX compression type: {0} is not supported.' + logger.error(msg.format(compression)) + raise Exception + + # if data is None, then it was not decoded or decompressed, so + # we assume here that it is going to be a bunch of tile elements + # TODO: this will/should raise an exception if there are no tiles + if encoding == next_gid is None: + def get_children(parent): + for child in parent.findall('tile'): + yield int(child.get('gid')) + + next_gid = get_children(data_node) + + elif data: + if type(data) == bytes: + fmt = struct.Struct('<L') + iterator = (data[i:i + 4] for i in range(0, len(data), 4)) + next_gid = (fmt.unpack(i)[0] for i in iterator) + else: + msg = 'layer data not in expected format ({})' + logger.error(msg.format(type(data))) + raise Exception + + init = lambda: [0] * self.width + reg = self.parent.register_gid + + # H (16-bit) may be a limitation for very detailed maps + self.data = tuple(array.array('H', init()) for i in range(self.height)) + for (y, x) in product(range(self.height), range(self.width)): + self.data[y][x] = reg(*decode_gid(next(next_gid))) + + return self + + +class TiledObject(TiledElement): + """ Represents a any Tiled Object + + Supported types: Box, Ellipse, Tile Object, Polyline, Polygon + """ + + def __init__(self, parent, node): + TiledElement.__init__(self) + self.parent = parent + + # defaults from the specification + self.name = None + self.type = None + self.x = 0 + self.y = 0 + self.width = 0 + self.height = 0 + self.rotation = 0 + self.gid = 0 + self.visible = 1 + + self.parse_xml(node) + + @property + def image(self): + if self.gid: + return self.parent.images[self.gid] + return None + + def parse_xml(self, node): + """ Parse an Object from ElementTree xml node + + :param node: ElementTree xml node + :return: self + """ + + def read_points(text): + """parse a text string of float tuples and return [(x,...),...] + """ + return tuple(tuple(map(float, i.split(','))) for i in text.split()) + + self._set_properties(node) + + # correctly handle "tile objects" (object with gid set) + if self.gid: + self.gid = self.parent.register_gid(self.gid) + + points = None + polygon = node.find('polygon') + if polygon is not None: + points = read_points(polygon.get('points')) + self.closed = True + + polyline = node.find('polyline') + if polyline is not None: + points = read_points(polyline.get('points')) + self.closed = False + + if points: + x1 = x2 = y1 = y2 = 0 + for x, y in points: + if x < x1: x1 = x + if x > x2: x2 = x + if y < y1: y1 = y + if y > y2: y2 = y + self.width = abs(x1) + abs(x2) + self.height = abs(y1) + abs(y2) + self.points = tuple( + [(i[0] + self.x, i[1] + self.y) for i in points]) + + return self + + +class TiledObjectGroup(TiledElement, list): + """ Represents a Tiled ObjectGroup + + Supports any operation of a normal list. + """ + + def __init__(self, parent, node): + TiledElement.__init__(self) + self.parent = parent + + # defaults from the specification + self.name = None + self.color = None + self.opacity = 1 + self.visible = 1 + + self.parse_xml(node) + + def parse_xml(self, node): + """ Parse an Object Group from ElementTree xml node + + :param node: ElementTree xml node + :return: self + """ + self._set_properties(node) + self.extend(TiledObject(self.parent, child) + for child in node.findall('object')) + + return self + + +class TiledImageLayer(TiledElement): + """ Represents Tiled Image Layer + + The image associated with this layer will be loaded and assigned a GID. + """ + + def __init__(self, parent, node): + TiledElement.__init__(self) + self.parent = parent + self.source = None + self.trans = None + self.gid = 0 + + # defaults from the specification + self.name = None + self.opacity = 1 + self.visible = 1 + + self.parse_xml(node) + + @property + def image(self): + if self.gid: + return self.parent.images[self.gid] + return None + + def parse_xml(self, node): + """ Parse an Image Layer from ElementTree xml node + + :param node: ElementTree xml node + :return: self + """ + self._set_properties(node) + self.name = node.get('name', None) + self.opacity = node.get('opacity', self.opacity) + self.visible = node.get('visible', self.visible) + image_node = node.find('image') + self.source = image_node.get('source') + self.trans = image_node.get('trans', None) + return self diff --git a/external/pytmx/readme.md b/external/pytmx/readme.md new file mode 100644 index 0000000..459d9c0 --- /dev/null +++ b/external/pytmx/readme.md @@ -0,0 +1,586 @@ +## PyTMX +##### For Python 2.7 and 3.3+ + +This is the most up-to-date version of PyTMX available and works with +Python 2.7 and 3.3+. + +If you have any problems or suggestions, please open an issue. +I am also often lurking #pygame on freenode. Feel free to contact me. + +Requires the six module. + +*Released under the LGPL v3* + +### See the "apps" folder for example use and cut/paste code. + + +News +=============================================================================== + +__11/13/15__ - Animations are now loaded +__07/08/15__ - Documentation overhaul +__04/18/15__ - Document support for pysdl2 and pyglet +__09/14/14__ - Merge python3 branch. Now 100% compatible with 2.7 and 3.3+ +__07/26/14__ - New python3/2 release. Check it out in the python3 branch. +__05/29/14__ - Added support for rotated objects and floating point +__04/04/14__ - New Six Branch created +__02/28/14__ - Image layer support, object points changed, new test.py! +__02/24/14__ - New Python 3 Support: see python3 branch +__02/06/14__ - Python 3 support coming soon + + +Introduction +=============================================================================== + +PyTMX is a map loader for python/pygame designed for games. It provides smart +tile loading with a fast and efficient storage base. Not only does it +correctly handle most Tiled object types, it also will load metadata for +them so you can modify your maps and objects in Tiled instead of modifying +your source code. + +New support for pysdl2 and pyglet! Check it out! + +Because PyTMX was built with games in mind, it differs slightly from Tiled in +a few minor aspects: + +- Layers not aligned to the grid are not supported. +- Some object metadata attribute names are not supported (see "Reserved Names") + +PyTMX strives to balance performance and flexibility. Feel free to use the +classes provided in pytmx.py as superclasses for your own maps, or simply +load the data with PyTMX and copy the data into your own classes with the api. + +There is no save feature. Once the map is loaded, it will be up to +you to provide a way to save changes to the map. I've used the pickle module +with good results. + +I need to clarify a few things: +- pytmx is not a rendering engine +- pytmx is not the Tiled Map Editor + + +Documentation +=============================================================================== + +This readme does include some detailed documentation, but the full API reference +and documentation can be found at the site below. For examples of real use, +check out the apps folder in this repo. The 'test' apps demonstrate how to +load maps, get layer, tile, and object data, as well as some rendering. + +http://pytmx.readthedocs.org/ + + +# Table of Contents +1. [Installation](#installation) +2. [Basic Use](#basic-use) +3. [Getting Properties](#object-properties) +4. [Working with Maps](#working-with-maps) +5. [Loading from XML](#loading-from-xml) +6. [Custom Image Loading](#custom-image-loading) +7. [Working with Tile Layers](#working-with-tile-layers) +8. [Getting Tile Animations](#getting-tile-animations) +9. [Working with Objects](#working-with-objects) +10. [Understanding Properties](#understanding-properties) + + +Getting Help +=============================================================================== + +For bugs or feature requests, please use the issues feature of github. For +all other general questions, join me on IRC at freennode.net #pygame. + + +Design Goals and Features +=============================================================================== + +* API with many handy functions +* Memory efficient and performant +* Loads data, "properties" metadata, and images from Tiled's TMX format +* Supports base64, csv, gzip, zlib and uncompressed XML formats +* Properties for all native Tiled object types +* Point data for polygon and polyline objects +* Automatic flipping and rotation of tiles +* Built-in image loading with pygame, pysdl2, and pyglet +* Loads animation information + + +Why use PyTMX? +=============================================================================== + +### PyTMX is efficient: +* Only the tiles used on a map are loaded into memory +* Map information is stored as integers, not python objects (32+kb) +* Extensive use of generators and iterators make it easy on memory +* Code is designed for compact size and readability + +### PyTMX is flexible: +* Supports all major Tiled features and object types +* PyTMX data classes can be extended +* Does not force you to render data in any particular way +* Includes many checks to give useful debugging information +* Supports pygame, pyglet, and pysdl2 image loading + +### PyTMX is supported: +* GitHub hosting allows for community participation +* I have kept PyTMX current with new versions of Tiled since v.7 + +### PyTMX is usable: +* Liberal LGPL license means you can use PyTMX for your project + + +Installation +=============================================================================== + +Install from pip + + pip install pytmx + + +You can also manually install it + + python setup.py install + + +Basic use: +=============================================================================== + +#### Just data, no images: + +```python +import pytmx +tiled_map = pytmx.TiledMap('map.tmx') +``` + +#### Load with pygame images: + +```python +from pytmx.util_pygame import load_pygame +tiled_map = load_pygame('map.tmx') +``` + +#### Load with pysdl2 images (experimental): + +```python +from pytmx.util_pysdl2 import load_pysdl2 +tiled_map = load_pysdl2('map.tmx') +``` + +#### Load with pyglet images (experimental): + +```python +from pytmx.util_pyglet import load_pyglet +tiled_map = load_pyglet('map.tmx') +``` + +#### Load from XML string: + +```python +import pytmx +tiled_map = pytmx.TiledMap.from_xml_string(some_string_here) +``` + +#### Iterate through layers and groups: + +```python +for layer in tiled_map.layers: + ... +``` + +#### Iterate through tile images in a tile layer: + +```python +for x, y, image in layer.tiles(): + ... +``` + +#### Iterate through Tiled objects in an object group: + +```python +for obj in layer: + ... +``` + +#### Get properties of various object types: + +```python + +# properties is a dict +TiledMap.properties +TiledTileLayer.properties['name'] +TiledObject.properties['type'] + +# tile ('GID') properties are accessed through the TiledMap: +properties = TiledMap.get_tile_properties(x, y, layer) +``` + +#### Get bounding box of an object: + +```python +bbox = obj.x, obj.y, obj.width, obj.height +``` + +#### Get the points/vertex to draw a polygon/polyline object: + +```python +points = obj.points +# if obj.closed == True, then obj is a polygon +``` + +Working with Maps +=============================================================================== + +TiledMap objects are returned from the loader. They contain layers, objects, +and a bunch of useful functions for getting information about the map. In +general, all of the pytmx types are not meant to be modified after being +returned from the loader. While there is a potentional for modifing them, +its not a supported function, and may change any time. Please consider them +read-only. + +Here is a list of attributes for use. (ie: TiledMap.layers): + +- layers: all layers in order +- tile_properties: dictionary of tile properties {GID: {props...}, ...} +- layernames: dictionary of layers with names: {name: layer, ...} +- images: list of all images in use, indexed by GID. Index 0 is always None. +- version +- orientation +- width: width of map in tiles, not pixels +- height: height of map in tiles, not pixels +- tileheight: height of tile in pixels. may differ between layers. +- tilewidth: width of tile in pixels. may differ between layers. +- background_color: map background color specified in Tiled +- properties: all user created properties about the map + + +#### Optional loading flags + +All loaders support the following flags: +- load_all_tiles: if True, all tiles will be loaded, even if unused +- invert_y: used for OpenGL graphics libs. Screen origin is at lower-left +- allow_duplicate_names: Force load maps with ambiguous data (see 'reserved names') + +```python +from pytmx.util_pygame import load_pygame +tiled_map = load_pygame(path_to_tmx_file, invert_y=True) +``` + +#### Loading from XML + +Most pytmx objects support loading from XML strings. For some objects, they require +references to other objects (like a layer has references to a tileset) and won't load +directly from XML. They can only be loaded if the entire map is loaded first. If you +want to store XML in a database or something, you can load the entire map with an XML string: + +```python +import pytmx +tiled_map = pytmx.TiledMap.from_xml_string(some_string_here) +``` + +#### Custom Image Loading + +The pytmx.TiledMap object constructor accepts an optional keyword "image_loader". The argument should be a function that accepts filename, colorkey (false, or a color) and pixelalpha (boolean) arguments. The function should return another function that will accept a rect-like object and any flags that the image loader might need to know, specific to the graphics library. Since that concept might be difficult to understand, I'll illustrate with some code. Use the following template code to load images from another graphics library. + + ```python +import pytmx + +def other_library_loader(filename, colorkey, **kwargs): + + # filename is a file to load an image from + # here you should load the image in whatever lib you want + + def extract_image(rect, flags): + + # rect is a (x, y, width, height) area where a particular tile is located + # flags is a named tuple that indicates how tile is flipped or rotated + + # use the rect to specify a region of the image file loaded in the function + # that encloses this one. + + # return an object to represent the tile + + # what is returned here will populate TiledMap.images, be returned by + # TiledObject.Image and included in TiledTileLayer.tiles() + + return extract_image + +level_map_and_images = pytmx.TiledMap("leveldata.tmx", image_loader=other_library_loader) +``` + +#### Accessing layers + +Layers are accessed through the TiledMap class and there are a few ways to get references to them: + +```python +# get a layer by name +layer_or_group = tiled_map.get_layer_by_name("base layer") + +# TiledMap.layers is a list of layers and groups +layer = tiled_map.layers[layer_index_number] + +# easily get references to just the visible tile layers +for layer in tiled_map.visible_tile_layers: + ... + +# just get references to visible object groups +for group in tile_map.visible_object_groups: + ... +``` + + +Working with Tile Layers +=============================================================================== + +Pytmx loads tile layers and their data: + +- name +- opacity +- visible: indicates if user has hidden the layer +- data: 2d array of all tile gids (normally not needed to use!) +- properties + +#### Tile Images + +Single tile images are accessible from TiledMap, TiledTileLayer, and TiledObject objects. +If you requre all images in a layer, there are more effecient ways described below. + +```python +# get image from the TiledMap using x, y, and layer numbers +pygame_surface = tile_map.get_tile_image(x, y, layer) + +# get tile image from an object with a image/GID assigned to it +image = obj.image + +# get image using gid (not needed for normal use!) +gid = layer.data[y][x] +image = tiled_map.images[gid] +``` + +#### Least effort involved getting all tile images + +```python +for x, y, image in layer.tiles(): + ... +``` + +#### Getting tile animations + +Tiled supports animated tiles, and pytmx has the ability to load them. +Animations are stored in the properties for the tile. The GID => image +conversion is already done for you. Animations from pytmx are a list +of AnimationFrame namedtuples. Please see the example below. + +```python +# just iterate over animated tiles and demo them + +# tmx_map is a TiledMap object +# tile_properties is a dictionary of all tile properties + +# iterate over the tile properties +for gid, props in tmx_map.tile_properties.items(): + + # iterate over the frames of the animation + for animation_frame in props['frames']: + + # do something with the image and duration of the frame + d = animation_frame.duration + i = animation_frame.image + ... + + # or just store the animation (list of frames) + my_anim = props['frames'] +``` + +#### If you really want to work with layer data directly... + +This information is provided for the curious, but for most people is not +required for normal use. + +Layer tiles are stored as a 'list of lists', or '2d array'. Each element of +layer data is a number which refers to a specific image in the map. These +numbers are called GID. Do not make references to these numbers, as they +will change if the map changes. + +Images for the GID can be accessed with the TiledMap.images list. + +With pygame, images will be plain pygame surfaces. These surfaces will be +checked for colorkey or per-pixel alpha automatically using information from +the TMX file and from checking each image for transparent pixels. You +do not need, and should not convert the tiles because it is already done. + +```python +layer = tiled_map.layers[0] +for x, y, gid in layer: + ... + +# get image using gid (not needed for normal use!) +# row index = 'y' +# column index = 'x' +image_gid = layer[row_index][column_index] +image = tiled_map.images[image_gid] + +# change GID of a position +layer[y][x] = new_gid +``` + +Working with Objects +=============================================================================== + +Tiled "objects" are things that are created in object layers, and include +polygons, polylings, boxes, ellispes, and tile objects. Pytmx loads all objects +and their data: + +- name +- type +- x +- y +- width +- height +- rotation +- gid (if it has an image) +- visible +- image +- properties + +#### Basics +Attributes x, y, width, and height all represent the bounding box of the object, +even polygons and polylines. + +#### Image Objects +If using a loader, then TiledObject.image will be a reference to the image used. + +#### Tile Objects +Tile Objects are objects that reference a tile in a tileset. These are loaded and +the image will be available to use. + +#### Polygon/Polyline Objects +These objects have special attributes: 'closed' and 'points'. Each point is (x, y) tuple. +If the object is a polygon, then TiledObject.closed will be True. Points are not +rotated if the rotation property is used. + +#### Accessing objects + +Objects can be accessed through the TiledMap or through a group. Object groups can be +used just like a python list, and support indexing, slicing, etc. + +```python +# search for an object with a specific name +my_object = tiled_map.get_object_by_name("baddy001") # will not return duplicates + +# get a group by name +group = tiled_map.get_layer_by_name("traps") + +# copy a group +traps = group[:] + +# iterate through objects in a group: +for obj in group: + ... +``` + +Understanding Properties +=============================================================================== + +Properties are a powerful feature of Tiled that allows the level designer to +assign key/value data to individual maps, tilesets, tiles, and objects. Pytmx +includes full support for reading this data so you can set parameters for stuff +in Tiled, instead of maintaining external data files, or even values in source. + +Properties are created by the user in tiled. There is also another set of data +that is part of each object, accessed by normal object attributes. This other +data is not set directly by the user, but is instead set by tiled. Typical +data that is object attributes are: 'name', 'x', 'opacity', or 'id'. + +If the user sets data for an object in Tiled, it becomes part of 'properties'. +'Properties' is just a normal python dictionary. + +```python +# get data normally set by Tiled +obj.name +obj.x +obj.opacity + +# get data set by the user in Tiled +obj.properties['hit points'] +obj.properties['goes to 11'] +``` + +Individual tile properties are accessed through the the parent map object: + +``` +tiled_map = TiledMap('level1.tmx') +props = tiled_map.get_tile_properties(x, y, layer) +props = tiled_map.get_tile_properties_by_gid(tile_gid) +``` + + +Scrolling Maps for Pygame +=============================================================================== + +I have another repo with a working demo of a proper scrolling map using Tiled +maps and pygame. Please feel free to test drive it. It isn't limited to Tiled +maps, you can use any data structure you want, as long as pygame is used. + +https://github.com/bitcraft/pyscroll + + +Reserved Names +================================================================================ + +Tiled supports user created metadata called "properties" for all the built-in +objects, like the map, tileset, objects, etc. Due to how the Tiled XML data is +stored, there are situations where Tiled internal metadata might have the same +name as user-created properties. + +Pytmx will raise a ValueError if it detects any conflicts. This check is +performed in order to prevent any situations where a level change might be made +in Tiled, but the programmer/designer doesn't know or forgets if the change was +made in the Tiled metadata or the user properties. + +I realize that it creates problems with certain common names like "id", or +"color". Overall, this check will help enforce clean design. + +However, If you really don't care about name conflicts, there is an option +you can try at your own risk. Pass 'allow_duplicate_names=True' to any +loader or to the TiledMap constructor and the checks will be disabled. + +```python +from pytmx.util_pygame import load_pygame +tiled_map = load_pygame('map.tmx', allow_duplicate_names=True) +``` + +In summary, don't use the following names when creating properties in Tiled: +As of 0.11.0, these values are: + +map: version, orientation, width, height, tilewidth, tileheight + properties, tileset, layer, objectgroup + +tileset: firstgid, source, name, tilewidth, tileheight, spacing, margin, + image, tile, properties + +tile: id, image, properties + +layer: name, x, y, width, height, opacity, properties, data + +objectgroup: name, color, x, y, width, height, opacity, object, properties + +object: id, name, type, x, y, width, height, gid, properties, polygon, + polyline, image + + +#### Please consider the following: + +PyTMX is a map __loader__. Pytmx takes the pain out of parsing XML, variable type conversion, shape loading, properties, and of course image loading. When asking for help, please understand that I want people to make their own games or utilities, and that PyTMX is able to make Tiled Maps easy to use. + +pytmx is not going to make your JRPG for you. You will need to do that yourself, and I, the author, cannot simply respond to every new developer who expects pytmx, pygame, or any other game library to simply make it work for them. Programming is a learned skill, and for most it takes practice and diligent study to get proficient at. I'm personally a nice guy, and do want to help, so before you flame me on your blog or reddit, understand what pytmx is used for, read the documentation and copy/paste the demo code if you have to. Thank you. + +I have a working solution to using Tiled Maps and Pygame ready for you. If you simply want a library to render the maps for you, please check it out, as they are designed to work together. + +http://github.com/bitcraft/pyscroll + + +Artwork Attributions +=============================================================================== +The 16x16 overworld tiles were created by MrBeast at opengameart.org. CC-BY 3.0 + +* If I missed your attribution, please let me know. + diff --git a/external/pytmx/util_pygame.py b/external/pytmx/util_pygame.py new file mode 100644 index 0000000..7744e80 --- /dev/null +++ b/external/pytmx/util_pygame.py @@ -0,0 +1,269 @@ +import logging +import itertools +import pytmx + +logger = logging.getLogger(__name__) +ch = logging.StreamHandler() +ch.setLevel(logging.INFO) +logger.addHandler(ch) +logger.setLevel(logging.INFO) + +try: + from pygame.transform import flip, rotate + import pygame +except ImportError: + logger.error('cannot import pygame (is it installed?)') + raise + +__all__ = ['load_pygame', 'pygame_image_loader', 'simplify', 'build_rects'] + + +def handle_transformation(tile, flags): + if flags.flipped_diagonally: + tile = flip(rotate(tile, 270), 1, 0) + if flags.flipped_horizontally or flags.flipped_vertically: + tile = flip(tile, flags.flipped_horizontally, flags.flipped_vertically) + return tile + + +def smart_convert(original, colorkey, pixelalpha): + """ + this method does several tests on a surface to determine the optimal + flags and pixel format for each tile surface. + + this is done for the best rendering speeds and removes the need to + convert() the images on your own + """ + tile_size = original.get_size() + threshold = 127 # the default + + # count the number of pixels in the tile that are not transparent + px = pygame.mask.from_surface(original, threshold).count() + + # there are no transparent pixels in the image + if px == tile_size[0] * tile_size[1]: + tile = original.convert() + + # there are transparent pixels, and tiled set a colorkey + elif colorkey: + tile = original.convert() + tile.set_colorkey(colorkey, pygame.RLEACCEL) + + # there are transparent pixels, and set for perpixel alpha + elif pixelalpha: + tile = original.convert_alpha() + + # there are transparent pixels, and we won't handle them + else: + tile = original.convert() + + return tile + + +def pygame_image_loader(filename, colorkey, **kwargs): + """ pytmx image loader for pygame + + :param filename: + :param colorkey: + :param kwargs: + :return: + """ + if colorkey: + colorkey = pygame.Color('#{0}'.format(colorkey)) + + pixelalpha = kwargs.get('pixelalpha', True) + image = pygame.image.load(filename) + + def load_image(rect=None, flags=None): + if rect: + try: + tile = image.subsurface(rect) + except ValueError: + logger.error('Tile bounds outside bounds of tileset image') + raise + else: + tile = image.copy() + + if flags: + tile = handle_transformation(tile, flags) + + tile = smart_convert(tile, colorkey, pixelalpha) + return tile + + return load_image + + +def load_pygame(filename, *args, **kwargs): + """ Load a TMX file, images, and return a TiledMap class + + PYGAME USERS: Use me. + + this utility has 'smart' tile loading. by default any tile without + transparent pixels will be loaded for quick blitting. if the tile has + transparent pixels, then it will be loaded with per-pixel alpha. this is + a per-tile, per-image check. + + if a color key is specified as an argument, or in the tmx data, the + per-pixel alpha will not be used at all. if the tileset's image has colorkey + transparency set in Tiled, the util_pygam will return images that have their + transparency already set. + + TL;DR: + Don't attempt to convert() or convert_alpha() the individual tiles. It is + already done for you. + """ + kwargs['image_loader'] = pygame_image_loader + return pytmx.TiledMap(filename, *args, **kwargs) + + +def build_rects(tmxmap, layer, tileset=None, real_gid=None): + """generate a set of non-overlapping rects that represents the distribution + of the specified gid. + + useful for generating rects for use in collision detection + + Use at your own risk: this is experimental...will change in future + + GID Note: You will need to add 1 to the GID reported by Tiled. + + :param tmxmap: TiledMap object + :param layer: int or string name of layer + :param tileset: int or string name of tileset + :param real_gid: Tiled GID of the tile + 1 (see note) + :return: List of pygame Rect objects + """ + if isinstance(tileset, int): + try: + tileset = tmxmap.tilesets[tileset] + except IndexError: + msg = "Tileset #{0} not found in map {1}." + logger.debug(msg.format(tileset, tmxmap)) + raise IndexError + + elif isinstance(tileset, str): + try: + tileset = [t for t in tmxmap.tilesets if t.name == tileset].pop() + except IndexError: + msg = "Tileset \"{0}\" not found in map {1}." + logger.debug(msg.format(tileset, tmxmap)) + raise ValueError + + elif tileset: + msg = "Tileset must be either a int or string. got: {0}" + logger.debug(msg.format(type(tileset))) + raise TypeError + + gid = None + if real_gid: + try: + gid, flags = tmxmap.map_gid(real_gid)[0] + except IndexError: + msg = "GID #{0} not found" + logger.debug(msg.format(real_gid)) + raise ValueError + + if isinstance(layer, int): + layer_data = tmxmap.get_layer_data(layer) + elif isinstance(layer, str): + try: + layer = [l for l in tmxmap.layers if l.name == layer].pop() + layer_data = layer.data + except IndexError: + msg = "Layer \"{0}\" not found in map {1}." + logger.debug(msg.format(layer, tmxmap)) + raise ValueError + + p = itertools.product(range(tmxmap.width), range(tmxmap.height)) + if gid: + points = [(x, y) for (x, y) in p if layer_data[y][x] == gid] + else: + points = [(x, y) for (x, y) in p if layer_data[y][x]] + + rects = simplify(points, tmxmap.tilewidth, tmxmap.tileheight) + return rects + + +def simplify(all_points, tilewidth, tileheight): + """Given a list of points, return list of rects that represent them + kludge: + + "A kludge (or kluge) is a workaround, a quick-and-dirty solution, + a clumsy or inelegant, yet effective, solution to a problem, typically + using parts that are cobbled together." + + -- wikipedia + + turn a list of points into a rects + adjacent rects will be combined. + + plain english: + the input list must be a list of tuples that represent + the areas to be combined into rects + the rects will be blended together over solid groups + + so if data is something like: + + 0 1 1 1 0 0 0 + 0 1 1 0 0 0 0 + 0 0 0 0 0 4 0 + 0 0 0 0 0 4 0 + 0 0 0 0 0 0 0 + 0 0 1 1 1 1 1 + + you'll have the 4 rects that mask the area like this: + + ..######...... + ..####........ + ..........##.. + ..........##.. + .............. + ....########## + + pretty cool, right? + + there may be cases where the number of rectangles is not as low as possible, + but I haven't found that it is excessively bad. certainly much better than + making a list of rects, one for each tile on the map! + """ + def pick_rect(points, rects): + ox, oy = sorted([(sum(p), p) for p in points])[0][1] + x = ox + y = oy + ex = None + + while 1: + x += 1 + if not (x, y) in points: + if ex is None: + ex = x - 1 + + if (ox, y + 1) in points: + if x == ex + 1: + y += 1 + x = ox + + else: + y -= 1 + break + else: + if x <= ex: y -= 1 + break + + c_rect = pygame.Rect(ox * tilewidth, oy * tileheight, + (ex - ox + 1) * tilewidth, + (y - oy + 1) * tileheight) + + rects.append(c_rect) + + rect = pygame.Rect(ox, oy, ex - ox + 1, y - oy + 1) + kill = [p for p in points if rect.collidepoint(p)] + [points.remove(i) for i in kill] + + if points: + pick_rect(points, rects) + + rect_list = [] + while all_points: + pick_rect(all_points, rect_list) + + return rect_list diff --git a/external/pytmx/util_pyglet.py b/external/pytmx/util_pyglet.py new file mode 100644 index 0000000..6b4a38a --- /dev/null +++ b/external/pytmx/util_pyglet.py @@ -0,0 +1,59 @@ +import logging + +logger = logging.getLogger(__name__) +ch = logging.StreamHandler() +ch.setLevel(logging.INFO) +logger.addHandler(ch) +logger.setLevel(logging.INFO) + +try: + import pyglet +except ImportError: + logger.error('cannot import pyglet (is it installed?)') + raise + +import pytmx + + +def pyglet_image_loader(filename, colorkey, **kwargs): + """basic image loading with pyglet + + returns pyglet Images, not textures + + This is a basic proof-of-concept and is likely to fail in some situations. + + Missing: + Transparency + Tile Rotation + + This is slow as well. + """ + if colorkey: + logger.debug('colorkey not implemented') + + image = pyglet.image.load(filename) + + def load_image(rect=None, flags=None): + if rect: + try: + x, y, w, h = rect + y = image.height - y - h + tile = image.get_region(x, y, w, h) + except: + logger.error('cannot get region %s of image', rect) + raise + else: + tile = image + + if flags: + logger.error('tile flags are not implemented') + + return tile + + return load_image + + +def load_pyglet(filename, *args, **kwargs): + kwargs['image_loader'] = pyglet_image_loader + kwargs['invert_y'] = True + return pytmx.TiledMap(filename, *args, **kwargs) diff --git a/external/pytmx/util_pysdl2.py b/external/pytmx/util_pysdl2.py new file mode 100644 index 0000000..a88793a --- /dev/null +++ b/external/pytmx/util_pysdl2.py @@ -0,0 +1,66 @@ +import logging +from functools import partial + +logger = logging.getLogger(__name__) +ch = logging.StreamHandler() +ch.setLevel(logging.INFO) +logger.addHandler(ch) +logger.setLevel(logging.INFO) + +try: + import sdl2.ext +except ImportError: + logger.error('cannot import pysdl2 (is it installed?)') + raise + +import pytmx + +__all__ = ['load_pysdl2', 'pysdl2_image_loader', ] +flag_names = ( + 'flipped_horizontally', + 'flipped_vertically', + 'flipped_diagonally',) + + +def pysdl2_image_loader(renderer, filename, colorkey, **kwargs): + def convert(surface): + texture_ = sdl2.SDL_CreateTextureFromSurface(renderer.renderer, surface) + sdl2.SDL_SetTextureBlendMode(texture_, sdl2.SDL_BLENDMODE_BLEND) + sdl2.SDL_FreeSurface(surface) + return texture_ + + def load_image(rect=None, flags=None): + if rect: + try: + flip = 0 + if flags.flipped_horizontally: + flip |= sdl2.SDL_FLIP_HORIZONTAL + if flags.flipped_vertically: + flip |= sdl2.SDL_FLIP_VERTICAL + if flags.flipped_diagonally: + flip |= 4 + + rect = sdl2.rect.SDL_Rect(*rect) + return texture, rect, flip + + except ValueError: + logger.error('Tile bounds outside bounds of tileset image') + raise + else: + return texture, None, 0 + + image = sdl2.ext.load_image(filename) + + if colorkey: + colorkey = sdl2.ext.string_to_color('#' + colorkey) + key = sdl2.SDL_MapRGB(image.format, *colorkey[:3]) + sdl2.SDL_SetColorKey(image, sdl2.SDL_TRUE, key) + + texture = convert(image) + + return load_image + + +def load_pysdl2(renderer, filename, *args, **kwargs): + kwargs['image_loader'] = partial(pysdl2_image_loader, renderer) + return pytmx.TiledMap(filename, *args, **kwargs) |