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