Source code for serge.blocks.tiled

"""Implements an interface to Tiled files"""

import math
import os
import pygame
from xml.etree.ElementTree import ElementTree

import serge.common
import serge.visual
import serge.geometry


[docs]class BadTiledFile(Exception): """The tiled file could not be found"""
[docs]class NotFound(Exception): """The object was not found"""
[docs]class BadLayer(Exception): """The layer specification was invalid"""
[docs]class SchemaNotMet(Exception): """An object schema was not met"""
LAYER_TYPES = ( 'visual', # Sprites 'adhoc-visual', # Object layer with sprite tiles placed on non-orthoganol grid 'movement', # Whether actors can move on this tile 'visibility', # Whether actors can see through this tile 'object', # Objects 'resistance', # Resistance to movement )
[docs]class TileMap(serge.common.Loggable): """A representation of a 2d map of tiles""" def __init__(self): """Initialise the Tiled""" self.addLogger() self.layers = [] TileMap.resetLayerTypes() @classmethod
[docs] def resetLayerTypes(cls): """Reset the layer types to default""" cls.layer_types = list(LAYER_TYPES)
@classmethod
[docs] def addLayerTypes(cls, layer_types): """Add more layer types""" cls.layer_types.extend(layer_types)
[docs] def addLayer(self, layer): """Add a layer""" self.layers.append(layer) return layer
[docs] def getLayers(self): """Return the layers of tiles""" return self.layers
[docs] def getLayersByType(self, type_name): """Return the layer with a given type""" return [layer for layer in self.getLayers() if layer.layer_type == type_name]
[docs] def getLayerByType(self, type_name): """Return the layer with a given type""" layers = self.getLayersByType(type_name) if not layers: raise NotFound('A layer with type "%s" was not found' % type_name) elif len(layers) > 1: raise BadLayer('Multiple layers with type "%s" were found' % type_name) else: return layers[0]
[docs] def getLayer(self, name): """return the tile with a certain name""" for layer in self.getLayers(): if layer.name == name: return layer raise NotFound('A layer named "%s" was not found' % name)
[docs] def getTypeFrom(self, name, properties): """Return the layer type, checking validity which we do it""" try: layer_type = properties['type'] except KeyError: raise BadLayer('Layer "%s" in file "%s" does not have a type property. Should be one of %s' % (name, self.filename, LAYER_TYPES)) # if layer_type in self.layer_types: return layer_type else: raise BadLayer('Layer "%s" in file "%s" has an invalid type property (%s). Should be one of %s' % (name, self.filename, layer_type, LAYER_TYPES))
[docs] def getPropertiesFrom(self, nodes): """Return a property disction from the node""" props = {} for node in nodes: text = node.attrib['value'] try: value = float(text) except ValueError: if text.lower() == 'true': value = True elif text.lower() == 'false': value = False else: value = text props[node.attrib['name']] = value return props
[docs] def getLayersForTile(self, (x, y), excluding=None): """Return a list of the layers that the tile at x, y is set on""" if excluding is None: excluding = [] return [layer for layer in self.getLayers() if layer.tiles[y][x] if layer.layer_type not in excluding]
[docs] def getPropertyBagArray(self, sprite_layers, boolean_layers, property_layers, prototype=None, optional_layers=None): """Return an array of property bags for the tile array You pass a series of lists of layer types, which are treated like: sprite_layers = tile based layers to treat as identifying sprites boolean_layers = tile layers where if a tile is set (to anything) then a boolean flag is True property_layers = tile layers where if a tile is set then the item recieves all the properties of the layer """ optional_layers = optional_layers if optional_layers is not None else [] # # Set the default item if prototype is None: prototype = serge.serialize.SerializedBag() # # Create an array to store the results data = [] for y in range(self.height): data.append([]) for x in range(self.width): cell = prototype.copy() cell.init() cell.coords = (x, y) cell.sprites = [] for boolean_type in boolean_layers: setattr(cell, boolean_type, False) data[-1].append(cell) # # Add sprites for sprite_type in sprite_layers: for layer in self.getLayersByType(sprite_type): for (x, y) in layer.iterCellLocations(): if layer.tiles[y][x]: data[y][x].sprites.append(self.getSpriteName(layer.tiles[y][x])) # # Add booleans for boolean_type in boolean_layers: try: layer = self.getLayerByType(boolean_type) except NotFound: if boolean_type in optional_layers: continue else: raise for (x, y) in layer.getLocationsWithTile(): setattr(data[y][x], boolean_type, True) # # Add properties for property_type in property_layers: for layer in self.getLayersByType(property_type): for (x, y) in layer.getLocationsWithTile(): for name, value in layer.properties.iteritems(): setattr(data[y][x], name, value) # return data
[docs] def getSize(self): """Return the size of the map using the first layer as a guide""" return self.getLayers()[0].getSize()
[docs]class Tiled(TileMap): """An interface to tiled files""" layer_types = list(LAYER_TYPES) def __init__(self, filename): """Initialise the Tiled""" super(Tiled, self).__init__() self.filename = filename self._registerSprites() self._parseLayers() self._parseObjectLayers()
[docs] def getSpriteName(self, idx): """Return the sprite name for an index""" last_end = 0 for name, ending_at in self.tilesets: if idx < ending_at: return '%s-%d' % (name, idx-last_end) last_end = ending_at-1 raise ValueError('Unknown sprite gid (%s)' % idx)
[docs] def getSpriteId(self, name): """Return the sprite id from a sprite name""" try: return self._allnames.index(name) + 1 except ValueError: raise ValueError('Unknown sprite name (%s)' % name)
def _registerSprites(self): """Register all the sprites""" self.log.info('Parsing sprites from %s' % self.filename) try: tree = ElementTree().parse(self.filename) except Exception, err: raise BadTiledFile('Unable to load XML file "%s": %s' % (self.filename, err)) # # Find all tilesets in the file self.tilesets = [] self._allnames = [] for tileset in tree.findall('.//tileset'): self.log.debug('Found tileset "%s"' % tileset.attrib['name']) width, height = int(tileset.attrib['tilewidth']), int(tileset.attrib['tileheight']) image = tileset.find('image') source = os.path.join(os.path.dirname(self.filename), image.attrib['source']) source_width, source_height = int(image.attrib['width']), int(image.attrib['height']) # # Create all images number = ((source_width*source_height)/(width*height)) names = ['%s-%d' % (tileset.attrib['name'], idx) for idx in range(1, number+1)] self._allnames.extend(names) serge.visual.Sprites.registerMultipleItems(names, source, source_width/width, source_height/height) self.log.debug('Created %d tiles' % number) # self.tilesets.append((tileset.attrib['name'], number+int(tileset.attrib['firstgid']))) def _parseLayers(self): """Parse the layers""" self.log.info('Parsing layers from %s' % self.filename) self.layers = layers = [] # try: tree = ElementTree().parse(self.filename) except Exception, err: raise BadTiledFile('Unable to load XML file "%s": %s' % (self.filename, err)) # self.width, self.height = int(tree.attrib['width']), int(tree.attrib['height']) # # Find all layers for layer in tree.findall('.//layer'): name = layer.attrib['name'] self.log.debug('Found layer "%s"' % name) width, height = int(layer.attrib['width']), int(layer.attrib['height']) # # Get all data data = [] for r, row in enumerate(layer.find('data').text.strip().split('\n')): data.append([]) for c, col in enumerate(row.rstrip(',').split(',')): data[-1].append(int(col)) # properties = self.getPropertiesFrom(layer.findall('properties/property')) layer_type = self.getTypeFrom(name, properties) self.addLayer(Layer(self, name, layer_type, width, height, data, properties)) def _parseObjectLayers(self): """Return the layers of objects""" self.log.info('Parsing object layers from %s' % self.filename) self.object_layers = [] # try: tree = ElementTree().parse(self.filename) except Exception, err: raise BadTiledFile('Unable to load XML file "%s": %s' % (self.filename, err)) # # Find all layers for layer in tree.findall('.//objectgroup'): name = layer.attrib['name'] self.log.debug('Found layer "%s"' % name) properties = self.getPropertiesFrom(layer.findall('properties/property')) width, height = int(layer.attrib.get('width', -1)), int(layer.attrib.get('height', -1)) new_layer = Layer(self, name, properties.get('type', 'object'), width, height, None, properties) # # Look for ad-hoc visual layer if properties.get('type', '') == 'adhoc-visual': self.layers.append(new_layer) # # Find sprites in this layer for obj in layer.findall('object'): new_layer.addObject(TileObject( 'tile', 'sprite', int(obj.attrib['x']), int(obj.attrib['y']), int(obj.attrib.get('width', 0)), int(obj.attrib.get('height', 0)), {}, self.getSpriteName(int(obj.attrib['gid'])))) else: self.object_layers.append(new_layer) # # Find objects in this layer for obj in layer.findall('object'): # # Watch out for polygon objects object_type = obj.attrib.get('type', 'unknown') children = obj.getchildren() points = None sprite_name = None # if children and children[0].tag in ('polygon', 'polyline'): try: point_string = children[0].attrib['points'] except Exception, err: raise BadLayer('Object "%s" on layer "%s" not recognized: %s' % ( obj.attrib.get('name', 'unknown'), name, err)) # # Convert points to integers and find the extent points = [map(float, coords.split(',')) for coords in point_string.split(' ')] x, y = zip(*points) min_x, max_x = min(x), max(x) min_y, max_y = min(y), max(y) x_pos, y_pos = float(obj.attrib['x']) + min_x, float(obj.attrib['y']) + min_y width = max_x - min_x height = max_y - min_y object_type = children[0].tag # # Correct points so that they are in absolute terms points = [[px + x_pos - min_x, py + y_pos - min_y] for px, py in points] else: # # Just get the width and height from the specified object width = float(obj.attrib.get('width', 0)) height = float(obj.attrib.get('height', 0)) x_pos, y_pos = float(obj.attrib['x']), float(obj.attrib['y']) # # Infer the type of the object sprite_id = int(obj.attrib.get('gid', 0)) if object_type == 'unknown': if sprite_id: object_type = 'sprite' sprite_name = self.getSpriteName(sprite_id) # # Create the new object new_layer.addObject(TileObject( obj.attrib.get('name', 'unknown'), object_type, x_pos, y_pos, width, height, self.getPropertiesFrom(obj.findall('properties/property')), points=points, sprite_name=sprite_name, sprite_id=sprite_id, rotation=float(obj.attrib.get('rotation', 0)) )) # # Add all the image layers self._parseImageLayers(tree) def _parseImageLayers(self, tree): """Parse out any image layers""" for layer in tree.findall('.//imagelayer'): name = layer.attrib['name'] properties = self.getPropertiesFrom(layer.findall('properties/property')) self.log.debug('Found image layer "%s"' % name) x_pos = float(layer.attrib['x']) y_pos = float(layer.attrib['y']) image = layer.find('image') source = os.path.join(os.path.split(self.filename)[0], image.attrib['source']) new_layer = Layer(self, name, properties.get('type', 'imagelayer'), -1, -1) new_object = TileImageObject(name, x_pos, y_pos, source, self.getPropertiesFrom(layer.findall('properties/property'))) new_layer.addObject(new_object) self.object_layers.append(new_layer)
[docs] def getObjectLayers(self): """Return the object layers""" return self.object_layers
[docs] def getObjectLayer(self, name): """Return an object layer with a certain name""" for layer in self.object_layers: if layer.name == name: return layer else: raise NotFound('Could not find object layer named "%s"' % name)
[docs] def getObjectLayersByType(self, type_name): """Return the object layers with a given type""" return [layer for layer in self.getObjectLayers() if layer.layer_type == type_name]
[docs]class Layer(serge.common.Loggable): """A layer in a tilemap""" def __init__(self, tiled, name, layer_type, width=None, height=None, tiles=None, properties=None): """Initialise the Layer""" self.addLogger() self.tiled = tiled self.name = name self.layer_type = layer_type # # Set initial size if width and height: self.width = width self.height = height elif tiles: self.width, self.height = len(tiles[0]), len(tiles) else: raise ValueError('Must initialise Layer with either width, height or tiles') # # Set the tiles if not tiles: self.tiles = [] for row in range(self.height): self.tiles.append([]) for col in range(self.width): self.tiles[-1].append(False) else: self.tiles = tiles # self.objects = [] # # Set properties self.properties = properties if properties is not None else {} for name, value in self.properties.iteritems(): setattr(self, name, value)
[docs] def getSize(self): """Return the size of the layer""" return (self.width, self.height)
[docs] def addObject(self, obj): """Add an object :param obj: the object to add to the layer """ self.objects.append(obj)
[docs] def getObjects(self): """Return all the objects""" return self.objects
[docs] def getObject(self, name): """Return the named object :param name: the name of the object to return """ for obj in self.getObjects(): if obj.name == name: return obj else: raise NotFound('Could not find object "%s" in layer "%s"' % (name, self.name))
[docs] def getSpriteFor(self, (x, y)): """Return the sprite for a certain location""" try: item = self.tiles[y][x] except IndexError: pass return serge.visual.Sprites.getItem(self.tiled.getSpriteName(item))
[docs] def setSpriteFor(self, (x, y), sprite): """Set a sprite at a certain position""" idx = self.tiled.getSpriteId(sprite.name) self.tiles[y][x] = idx
[docs] def getLocationsWithTile(self): """Return all tile locations with a tile""" matches = [] for y, row in enumerate(self.tiles): for x, item in enumerate(row): if item != 0: matches.append((x, y)) return matches
[docs] def getLocationsWithSpriteName(self, sprite_name): """Return all tile locations with a specific tile :param sprite_name: the name of the sprite you are looking for """ matches = [] for y, row in enumerate(self.tiles): for x, item in enumerate(row): if item and self.tiled.getSpriteName(item) == sprite_name: matches.append((x, y)) return matches
[docs] def getLocationsWithoutTile(self): """Return all tile locations without a tile""" matches = [] for y, row in enumerate(self.tiles): for x, item in enumerate(row): if item == 0: matches.append((x, y)) return matches
[docs] def iterCellLocations(self): """Return an interation of the cell locations""" for y, row in enumerate(self.tiles): for x, item in enumerate(row): yield (x, y)
[docs] def validateSchemas(self, schema, strict_mode=False): """Validate that all objects with a given type have the right properties""" failures = [] for obj in self.getObjects(): if obj.object_type != 'unknown': if obj.object_type in schema: for name in schema[obj.object_type]: if not hasattr(obj, name) and not name.startswith('?'): failures.append('%s(%s) has no "%s"' % (obj.name, obj.object_type, name)) if strict_mode: for name in obj.properties: if name not in schema[obj.object_type] and ('?' + name) not in schema[obj.object_type]: failures.append('%s(%s) shouldn\'t have "%s"' % (obj.name, obj.object_type, name)) else: failures.append('%s has an unknown type (%s)' % (obj.name, obj.object_type)) elif strict_mode: failures.append('%s has no type' % obj.name) # if not failures: return True else: raise SchemaNotMet('Schema validation failed: %s' % ', '.join(failures))
[docs]class TileObject(serge.geometry.Rectangle, serge.common.Loggable): """A tile""" def __init__(self, name, object_type, x, y, width, height, properties, sprite_name=None, sprite_id=None, points=None, rotation=0): """Initialise the tile""" self.name = name self.object_type = object_type self.sprite_name = sprite_name self.sprite_id = sprite_id self.points = points self.rotation = rotation # if object_type == 'sprite': if rotation == 0: # # Correct for the fact that Tiled returns the bottom left of the image self.setSpatial(x, y - height, width, height) else: # # Tiled gives the x and y as the bottom right of the # shape. We correct for the rotation to determine the center point # alpha = math.radians(rotation) beta = math.atan(float(height) / width) gamma = beta - alpha # l = math.sqrt((width / 2.0) ** 2 + (height / 2.0) ** 2) dx = l * math.cos(gamma) dy = l * math.sin(gamma) # cx = x + dx cy = y - dy correct_width = height * math.sin(alpha) + width * math.cos(alpha) correct_height = height * math.cos(alpha) + width * math.sin(alpha) # self.setSpatialCentered(cx, cy, correct_width, correct_height) else: self.setSpatial(x, y, width, height) # # Set properties self.properties = properties for name, value in properties.iteritems(): setattr(self, name, value)
[docs] def getStringTuple(self, attribute_name, default): """Return a tuple of a string comma separated list""" if not hasattr(self, attribute_name): return default else: return tuple(map(float, getattr(self, attribute_name).split(',')))
[docs]class TileImageObject(TileObject): """Initialise the object""" def __init__(self, name, x, y, source, properties): """Initialise the image""" self.surface = pygame.image.load(source) width, height = self.surface.get_size() self.filename = source super(TileImageObject, self).__init__(name, 'image', x, y, width, height, properties)
[docs] def getSurface(self): """"Return the surface""" return self.surface