def updateCurrentTiles(self): view = self.currentTilesetView() if (not view): return s = view.selectionModel() if (not s): return indexes = s.selection().indexes() if len(indexes) == 0: return first = indexes[0] minX = first.column() maxX = first.column() minY = first.row() maxY = first.row() for index in indexes: if minX > index.column(): minX = index.column() if maxX < index.column(): maxX = index.column() if minY > index.row(): minY = index.row() if maxY < index.row(): maxY = index.row() # Create a tile layer from the current selection tileLayer = TileLayer(QString(), 0, 0, maxX - minX + 1, maxY - minY + 1) model = view.tilesetModel() for index in indexes: tileLayer.setCell(index.column() - minX, index.row() - minY, Cell(model.tileAt(index))) self.setCurrentTiles(tileLayer)
def read(self, fileName): uncompressed = QByteArray() # Read data f = QFile(fileName) if (f.open(QIODevice.ReadOnly)): compressed = f.readAll() f.close() uncompressed, length = decompress(compressed, 48 * 48) # Check the data if (uncompressed.count() != 48 * 48): self.mError = self.tr("This is not a valid Droidcraft map file!") return None uncompressed = uncompressed.data() # Build 48 x 48 map # Create a Map -> Create a Tileset -> Add Tileset to map # -> Create a TileLayer -> Fill layer -> Add TileLayer to Map map = Map(Map.Orientation.Orthogonal, 48, 48, 32, 32) mapTileset = Tileset.create("tileset", 32, 32) mapTileset.loadFromImage(QImage(":/tileset.png"), "tileset.png") map.addTileset(mapTileset) # Fill layer mapLayer = TileLayer("map", 0, 0, 48, 48) # Load for i in range(0, 48 * 48): tileFile = int(uncompressed[i]) & 0xff y = int(i / 48) x = i - (48 * y) tile = mapTileset.tileAt(tileFile) mapLayer.setCell(x, y, Cell(tile)) map.addLayer(mapLayer) return map
def createNewMapObject(self): if (not self.mTile): return None newMapObject = MapObject() newMapObject.setShape(MapObject.Rectangle) newMapObject.setCell(Cell(self.mTile)) newMapObject.setSize(self.mTile.size()) return newMapObject
def cellAt(self, *args): l = len(args) if l==1: pos = args[0] return self.cellAt(pos.x(), pos.y()) elif l==2: x, y = args layerX = x - self.mTileLayer.x() layerY = y - self.mTileLayer.y() if (not self.mTileLayer.contains(layerX, layerY)): return Cell() return self.mTileLayer.cellAt(layerX, layerY)
def gidToCell(self, gid): result = Cell() # Read out the flags result.flippedHorizontally = (gid & FlippedHorizontallyFlag) result.flippedVertically = (gid & FlippedVerticallyFlag) result.flippedAntiDiagonally = (gid & FlippedAntiDiagonallyFlag) # Clear the flags gid &= ~(FlippedHorizontallyFlag | FlippedVerticallyFlag | FlippedAntiDiagonallyFlag) if (gid == 0): ok = True elif (self.isEmpty()): ok = False else: # Find the tileset containing this tile index = self.mFirstGidToTileset.upperBound(gid) if index==0: ok = False else: item = self.mFirstGidToTileset.itemByIndex(index-1) # Navigate one tileset back since upper bound finds the next tileId = gid - item[0] tileset = item[1] columnCount = self.mTilesetColumnCounts.value(tileset, 0) if (columnCount > 0 and columnCount != tileset.columnCount()): # Correct tile index for changes in image width row = int(tileId / columnCount) column = int(tileId % columnCount) tileId = row * tileset.columnCount() + column result.tile = tileset.tileAt(tileId) ok = True return result, ok
def __init__(self, *args): super().__init__(Object.MapObjectType) self.mPolygon = QPolygonF() self.mName = QString() self.mPos = QPointF() self.mCell = Cell() self.mType = QString() self.mId = 0 self.mShape = MapObject.Rectangle self.mObjectGroup = None self.mRotation = 0.0 self.mVisible = True l = len(args) if l == 0: self.mSize = QSizeF(0, 0) elif l == 4: name, _type, pos, size = args self.mName = name self.mType = _type self.mPos = pos self.mSize = QSizeF(size)
def setTile(self, tile): if type(tile) == list: tile = tile[0] if (self.mTile == tile): return self.mTile = tile self.mMapScene.disableSelectedTool() previousDocument = self.mMapScene.mapDocument() if (tile): self.mMapView.setEnabled(not self.mTile.tileset().isExternal()) map = Map(Map.Orientation.Orthogonal, 1, 1, tile.width(), tile.height()) map.addTileset(tile.sharedTileset()) tileLayer = TileLayer(QString(), 0, 0, 1, 1) tileLayer.setCell(0, 0, Cell(tile)) map.addLayer(tileLayer) objectGroup = None if (tile.objectGroup()): objectGroup = tile.objectGroup().clone() else: objectGroup = ObjectGroup() objectGroup.setDrawOrder(ObjectGroup.DrawOrder.IndexOrder) map.addLayer(objectGroup) mapDocument = MapDocument(map) self.mMapScene.setMapDocument(mapDocument) self.mToolManager.setMapDocument(mapDocument) mapDocument.setCurrentLayerIndex(1) self.mMapScene.enableSelectedTool() mapDocument.undoStack().indexChanged.connect(self.applyChanges) else: self.mMapView.setEnabled(False) self.mMapScene.setMapDocument(None) self.mToolManager.setMapDocument(None) if (previousDocument): previousDocument.undoStack().disconnect() del previousDocument
def __init__(self, *args): super().__init__(Object.MapObjectType) self.mPolygon = QPolygonF() self.mName = QString() self.mPos = QPointF() self.mCell = Cell() self.mType = QString() self.mId = 0 self.mShape = MapObject.Rectangle self.mObjectGroup = None self.mRotation = 0.0 self.mVisible = True l = len(args) if l==0: self.mSize = QSizeF(0, 0) elif l==4: name, _type, pos, size = args self.mName = name self.mType = _type self.mPos = pos self.mSize = QSizeF(size)
class MapObject(Object): ## # Enumerates the different object shapes. Rectangle is the default shape. # When a polygon is set, the shape determines whether it should be # interpreted as a filled polygon or a line. ## Rectangle, Polygon, Polyline, Ellipse = range(4) def __init__(self, *args): super().__init__(Object.MapObjectType) self.mPolygon = QPolygonF() self.mName = QString() self.mPos = QPointF() self.mCell = Cell() self.mType = QString() self.mId = 0 self.mShape = MapObject.Rectangle self.mObjectGroup = None self.mRotation = 0.0 self.mVisible = True l = len(args) if l==0: self.mSize = QSizeF(0, 0) elif l==4: name, _type, pos, size = args self.mName = name self.mType = _type self.mPos = pos self.mSize = QSizeF(size) ## # Returns the id of this object. Each object gets an id assigned that is # unique for the map the object is on. ## def id(self): return self.mId ## # Sets the id of this object. ## def setId(self, id): self.mId = id ## # Returns the name of this object. The name is usually just used for # identification of the object in the editor. ## def name(self): return self.mName ## # Sets the name of this object. ## def setName(self, name): self.mName = name ## # Returns the type of this object. The type usually says something about # how the object is meant to be interpreted by the engine. ## def type(self): return self.mType ## # Sets the type of this object. ## def setType(self, type): self.mType = type ## # Returns the position of this object. ## def position(self): return QPointF(self.mPos) ## # Sets the position of this object. ## def setPosition(self, pos): self.mPos = pos ## # Returns the x position of this object. ## def x(self): return self.mPos.x() ## # Sets the x position of this object. ## def setX(self, x): self.mPos.setX(x) ## # Returns the y position of this object. ## def y(self): return self.mPos.y() ## # Sets the x position of this object. ## def setY(self, y): self.mPos.setY(y) ## # Returns the size of this object. ## def size(self): return self.mSize ## # Sets the size of this object. ## def setSize(self, *args): l = len(args) if l==1: size = args[0] self.mSize = QSizeF(size) elif l==2: width, height = args self.setSize(QSizeF(width, height)) ## # Returns the width of this object. ## def width(self): return self.mSize.width() ## # Sets the width of this object. ## def setWidth(self, width): self.mSize.setWidth(width) ## # Returns the height of this object. ## def height(self): return self.mSize.height() ## # Sets the height of this object. ## def setHeight(self, height): self.mSize.setHeight(height) ## # Sets the polygon associated with this object. The polygon is only used # when the object shape is set to either Polygon or Polyline. # # \sa setShape() ## def setPolygon(self, polygon): self.mPolygon = polygon ## # Returns the polygon associated with this object. Returns an empty # polygon when no polygon is associated with this object. ## def polygon(self): return QPolygonF(self.mPolygon) ## # Sets the shape of the object. ## def setShape(self, shape): self.mShape = shape ## # Returns the shape of the object. ## def shape(self): return self.mShape ## # Shortcut to getting a QRectF from position() and size(). ## def bounds(self): return QRectF(self.mPos, self.mSize) ## # Shortcut to getting a QRectF from position() and size() that uses cell tile if present. ## def boundsUseTile(self): if (self.mCell.isEmpty()): # No tile so just use regular bounds return self.bounds() # Using the tile for determing boundary # Note the position given is the bottom-left corner so correct for that return QRectF(QPointF(self.mPos.x(), self.mPos.y() - self.mCell.tile.height()), self.mCell.tile.size()) ## # Sets the tile that is associated with this object. The object will # display as the tile image. # # \warning The object shape is ignored for tile objects! ## def setCell(self, cell): self.mCell = cell ## # Returns the tile associated with this object. ## def cell(self): return self.mCell ## # Returns the object group this object belongs to. ## def objectGroup(self): return self.mObjectGroup ## # Sets the object group this object belongs to. Should only be called # from the ObjectGroup class. ## def setObjectGroup(self, objectGroup): self.mObjectGroup = objectGroup ## # Returns the rotation of the object in degrees. ## def rotation(self): return self.mRotation ## # Sets the rotation of the object in degrees. ## def setRotation(self, rotation): self.mRotation = rotation ## # This is somewhat of a workaround for dealing with the ways different objects # align. # # Traditional rectangle objects have top-left alignment. # Tile objects have bottom-left alignment on orthogonal maps, but # bottom-center alignment on isometric maps. # # Eventually, the object alignment should probably be configurable. For # backwards compatibility, it will need to be configurable on a per-object # level. ## def alignment(self): if (self.mCell.isEmpty()): return Alignment.TopLeft elif (self.mObjectGroup): map = self.mObjectGroup.map() if map: if (map.orientation() == Map.Orientation.Isometric): return Alignment.Bottom return Alignment.BottomLeft def isVisible(self): return self.mVisible def setVisible(self, visible): self.mVisible = visible ## # Flip this object in the given \a direction. This doesn't change the size # of the object. ## def flip(self, direction): if (not self.mCell.isEmpty()): if (direction == FlipDirection.FlipHorizontally): self.mCell.flippedHorizontally = not self.mCell.flippedHorizontally elif (direction == FlipDirection.FlipVertically): self.mCell.flippedVertically = not self.mCell.flippedVertically if (not self.mPolygon.isEmpty()): center2 = self.mPolygon.boundingRect().center() * 2 if (direction == FlipDirection.FlipHorizontally): for i in range(self.mPolygon.size()): # oh, QPointF mPolygon returned is a copy of internal object self.mPolygon[i] = QPointF(center2.x() - self.mPolygon[i].x(), self.mPolygon[i].y()) elif (direction == FlipDirection.FlipVertically): for i in range(self.mPolygon.size()): self.mPolygon[i] = QPointF(self.mPolygon[i].x(), center2.y() - self.mPolygon[i].y()) ## # Returns a duplicate of this object. The caller is responsible for the # ownership of this newly created object. ## def clone(self): o = MapObject(self.mName, self.mType, self.mPos, self.mSize) o.setProperties(self.properties()) o.setPolygon(self.mPolygon) o.setShape(self.mShape) o.setCell(self.mCell) o.setRotation(self.mRotation) return o
def updateBrush(self, cursorPos, _list=None): # get the current tile layer currentLayer = self.currentTileLayer() layerWidth = currentLayer.width() layerHeight = currentLayer.height() numTiles = layerWidth * layerHeight paintCorner = 0 # if we are in vertex paint mode, the bottom right corner on the map will appear as an invalid tile offset... if (self.mBrushMode == BrushMode.PaintVertex): if (cursorPos.x() == layerWidth): cursorPos.setX(cursorPos.x() - 1) paintCorner |= 1 if (cursorPos.y() == layerHeight): cursorPos.setY(cursorPos.y() - 1) paintCorner |= 2 # if the cursor is outside of the map, bail out if (not currentLayer.bounds().contains(cursorPos)): return terrainTileset = None terrainId = -1 if (self.mTerrain): terrainTileset = self.mTerrain.tileset() terrainId = self.mTerrain.id() # allocate a buffer to build the terrain tilemap (TODO: this could be retained per layer to save regular allocation) newTerrain = [] for i in range(numTiles): newTerrain.append(0) # allocate a buffer of flags for each tile that may be considered (TODO: this could be retained per layer to save regular allocation) checked = array('B') for i in range(numTiles): checked.append(0) # create a consideration list, and push the start points transitionList = QList() initialTiles = 0 if (_list): # if we were supplied a list of start points for p in _list: transitionList.append(p) initialTiles += 1 else: transitionList.append(cursorPos) initialTiles = 1 brushRect = QRect(cursorPos, cursorPos) # produce terrain with transitions using a simple, relative naive approach (considers each tile once, and doesn't allow re-consideration if selection was bad) while (not transitionList.isEmpty()): # get the next point in the consideration list p = transitionList.takeFirst() x = p.x() y = p.y() i = y * layerWidth + x # if we have already considered this point, skip to the next # TODO: we might want to allow re-consideration if prior tiles... but not for now, this would risk infinite loops if (checked[i]): continue tile = currentLayer.cellAt(p).tile currentTerrain = terrain(tile) # get the tileset for this tile tileset = None if (terrainTileset): # if we are painting a terrain, then we'll use the terrains tileset tileset = terrainTileset elif (tile): # if we're erasing terrain, use the individual tiles tileset (to search for transitions) tileset = tile.tileset() else: # no tile here and we're erasing terrain, not much we can do continue # calculate the ideal tile for this position preferredTerrain = 0xFFFFFFFF mask = 0 if (initialTiles): # for the initial tiles, we will insert the selected terrain and add the surroundings for consideration if (self.mBrushMode == BrushMode.PaintTile): # set the whole tile to the selected terrain preferredTerrain = makeTerrain(terrainId) mask = 0xFFFFFFFF else: # Bail out if encountering a tile from a different tileset if (tile and tile.tileset() != tileset): continue # calculate the corner mask mask = 0xFF << (3 - paintCorner) * 8 # mask in the selected terrain preferredTerrain = (currentTerrain & ~mask) | (terrainId << (3 - paintCorner) * 8) initialTiles -= 1 # if there's nothing to paint... skip this tile if (preferredTerrain == currentTerrain and (not tile or tile.tileset() == tileset)): continue else: # Bail out if encountering a tile from a different tileset if (tile and tile.tileset() != tileset): continue # following tiles each need consideration against their surroundings preferredTerrain = currentTerrain mask = 0 # depending which connections have been set, we update the preferred terrain of the tile accordingly if (y > 0 and checked[i - layerWidth]): preferredTerrain = ( (terrain(newTerrain[i - layerWidth]) << 16) | (preferredTerrain & 0x0000FFFF)) & 0xFFFFFFFF mask |= 0xFFFF0000 if (y < layerHeight - 1 and checked[i + layerWidth]): preferredTerrain = ( (terrain(newTerrain[i + layerWidth]) >> 16) | (preferredTerrain & 0xFFFF0000)) & 0xFFFFFFFF mask |= 0x0000FFFF if (x > 0 and checked[i - 1]): preferredTerrain = ( ((terrain(newTerrain[i - 1]) << 8) & 0xFF00FF00) | (preferredTerrain & 0x00FF00FF)) & 0xFFFFFFFF mask |= 0xFF00FF00 if (x < layerWidth - 1 and checked[i + 1]): preferredTerrain = ( ((terrain(newTerrain[i + 1]) >> 8) & 0x00FF00FF) | (preferredTerrain & 0xFF00FF00)) & 0xFFFFFFFF mask |= 0x00FF00FF # find the most appropriate tile in the tileset paste = None if (preferredTerrain != 0xFFFFFFFF): paste = findBestTile(tileset, preferredTerrain, mask) if (not paste): continue # add tile to the brush newTerrain[i] = paste checked[i] = True # expand the brush rect to fit the edit set brushRect |= QRect(p, p) # consider surrounding tiles if terrain constraints were not satisfied if (y > 0 and not checked[i - layerWidth]): above = currentLayer.cellAt(x, y - 1).tile if (topEdge(paste) != bottomEdge(above)): transitionList.append(QPoint(x, y - 1)) if (y < layerHeight - 1 and not checked[i + layerWidth]): below = currentLayer.cellAt(x, y + 1).tile if (bottomEdge(paste) != topEdge(below)): transitionList.append(QPoint(x, y + 1)) if (x > 0 and not checked[i - 1]): left = currentLayer.cellAt(x - 1, y).tile if (leftEdge(paste) != rightEdge(left)): transitionList.append(QPoint(x - 1, y)) if (x < layerWidth - 1 and not checked[i + 1]): right = currentLayer.cellAt(x + 1, y).tile if (rightEdge(paste) != leftEdge(right)): transitionList.append(QPoint(x + 1, y)) # create a stamp for the terrain block stamp = TileLayer(QString(), brushRect.left(), brushRect.top(), brushRect.width(), brushRect.height()) for y in range(brushRect.top(), brushRect.bottom() + 1): for x in range(brushRect.left(), brushRect.right() + 1): i = y * layerWidth + x if (not checked[i]): continue tile = newTerrain[i] if (tile): stamp.setCell(x - brushRect.left(), y - brushRect.top(), Cell(tile)) else: # TODO: we need to do something to erase tiles where checked[i] is True, and newTerrain[i] is NULL # is there an eraser stamp? investigate how the eraser works... pass # set the new tile layer as the brush self.brushItem().setTileLayer(stamp) del checked del newTerrain self.mPaintX = cursorPos.x() self.mPaintY = cursorPos.y() self.mOffsetX = cursorPos.x() - brushRect.left() self.mOffsetY = cursorPos.y() - brushRect.top()
class MapObject(Object): ## # Enumerates the different object shapes. Rectangle is the default shape. # When a polygon is set, the shape determines whether it should be # interpreted as a filled polygon or a line. ## Rectangle, Polygon, Polyline, Ellipse = range(4) def __init__(self, *args): super().__init__(Object.MapObjectType) self.mPolygon = QPolygonF() self.mName = QString() self.mPos = QPointF() self.mCell = Cell() self.mType = QString() self.mId = 0 self.mShape = MapObject.Rectangle self.mObjectGroup = None self.mRotation = 0.0 self.mVisible = True l = len(args) if l == 0: self.mSize = QSizeF(0, 0) elif l == 4: name, _type, pos, size = args self.mName = name self.mType = _type self.mPos = pos self.mSize = QSizeF(size) ## # Returns the id of this object. Each object gets an id assigned that is # unique for the map the object is on. ## def id(self): return self.mId ## # Sets the id of this object. ## def setId(self, id): self.mId = id ## # Returns the name of this object. The name is usually just used for # identification of the object in the editor. ## def name(self): return self.mName ## # Sets the name of this object. ## def setName(self, name): self.mName = name ## # Returns the type of this object. The type usually says something about # how the object is meant to be interpreted by the engine. ## def type(self): return self.mType ## # Sets the type of this object. ## def setType(self, type): self.mType = type ## # Returns the position of this object. ## def position(self): return QPointF(self.mPos) ## # Sets the position of this object. ## def setPosition(self, pos): self.mPos = pos ## # Returns the x position of this object. ## def x(self): return self.mPos.x() ## # Sets the x position of this object. ## def setX(self, x): self.mPos.setX(x) ## # Returns the y position of this object. ## def y(self): return self.mPos.y() ## # Sets the x position of this object. ## def setY(self, y): self.mPos.setY(y) ## # Returns the size of this object. ## def size(self): return self.mSize ## # Sets the size of this object. ## def setSize(self, *args): l = len(args) if l == 1: size = args[0] self.mSize = QSizeF(size) elif l == 2: width, height = args self.setSize(QSizeF(width, height)) ## # Returns the width of this object. ## def width(self): return self.mSize.width() ## # Sets the width of this object. ## def setWidth(self, width): self.mSize.setWidth(width) ## # Returns the height of this object. ## def height(self): return self.mSize.height() ## # Sets the height of this object. ## def setHeight(self, height): self.mSize.setHeight(height) ## # Sets the polygon associated with this object. The polygon is only used # when the object shape is set to either Polygon or Polyline. # # \sa setShape() ## def setPolygon(self, polygon): self.mPolygon = polygon ## # Returns the polygon associated with this object. Returns an empty # polygon when no polygon is associated with this object. ## def polygon(self): return QPolygonF(self.mPolygon) ## # Sets the shape of the object. ## def setShape(self, shape): self.mShape = shape ## # Returns the shape of the object. ## def shape(self): return self.mShape ## # Shortcut to getting a QRectF from position() and size(). ## def bounds(self): return QRectF(self.mPos, self.mSize) ## # Shortcut to getting a QRectF from position() and size() that uses cell tile if present. ## def boundsUseTile(self): if (self.mCell.isEmpty()): # No tile so just use regular bounds return self.bounds() # Using the tile for determing boundary # Note the position given is the bottom-left corner so correct for that return QRectF( QPointF(self.mPos.x(), self.mPos.y() - self.mCell.tile.height()), self.mCell.tile.size()) ## # Sets the tile that is associated with this object. The object will # display as the tile image. # # \warning The object shape is ignored for tile objects! ## def setCell(self, cell): self.mCell = cell ## # Returns the tile associated with this object. ## def cell(self): return self.mCell ## # Returns the object group this object belongs to. ## def objectGroup(self): return self.mObjectGroup ## # Sets the object group this object belongs to. Should only be called # from the ObjectGroup class. ## def setObjectGroup(self, objectGroup): self.mObjectGroup = objectGroup ## # Returns the rotation of the object in degrees. ## def rotation(self): return self.mRotation ## # Sets the rotation of the object in degrees. ## def setRotation(self, rotation): self.mRotation = rotation ## # This is somewhat of a workaround for dealing with the ways different objects # align. # # Traditional rectangle objects have top-left alignment. # Tile objects have bottom-left alignment on orthogonal maps, but # bottom-center alignment on isometric maps. # # Eventually, the object alignment should probably be configurable. For # backwards compatibility, it will need to be configurable on a per-object # level. ## def alignment(self): if (self.mCell.isEmpty()): return Alignment.TopLeft elif (self.mObjectGroup): map = self.mObjectGroup.map() if map: if (map.orientation() == Map.Orientation.Isometric): return Alignment.Bottom return Alignment.BottomLeft def isVisible(self): return self.mVisible def setVisible(self, visible): self.mVisible = visible ## # Flip this object in the given \a direction. This doesn't change the size # of the object. ## def flip(self, direction): if (not self.mCell.isEmpty()): if (direction == FlipDirection.FlipHorizontally): self.mCell.flippedHorizontally = not self.mCell.flippedHorizontally elif (direction == FlipDirection.FlipVertically): self.mCell.flippedVertically = not self.mCell.flippedVertically if (not self.mPolygon.isEmpty()): center2 = self.mPolygon.boundingRect().center() * 2 if (direction == FlipDirection.FlipHorizontally): for i in range(self.mPolygon.size()): # oh, QPointF mPolygon returned is a copy of internal object self.mPolygon[i] = QPointF( center2.x() - self.mPolygon[i].x(), self.mPolygon[i].y()) elif (direction == FlipDirection.FlipVertically): for i in range(self.mPolygon.size()): self.mPolygon[i] = QPointF( self.mPolygon[i].x(), center2.y() - self.mPolygon[i].y()) ## # Returns a duplicate of this object. The caller is responsible for the # ownership of this newly created object. ## def clone(self): o = MapObject(self.mName, self.mType, self.mPos, self.mSize) o.setProperties(self.properties()) o.setPolygon(self.mPolygon) o.setShape(self.mShape) o.setCell(self.mCell) o.setRotation(self.mRotation) return o
def read(self, fileName): # Read data. file = QFile(fileName) if (not file.open(QIODevice.ReadOnly)): self.mError = self.tr("Cannot open Replica Island map file!") return 0 _in = QDataStream(file) _in.setByteOrder(QDataStream.LittleEndian) _in.setFloatingPointPrecision(QDataStream.SinglePrecision) # Parse file header. mapSignature = _in.readUInt8() layerCount = _in.readUInt8() backgroundIndex = _in.readUInt8() if (_in.status() == QDataStream.ReadPastEnd or mapSignature != 96): self.mError = self.tr("Can't parse file header!") return 0 # Create our map, setting width and height to 0 until we load a layer. map = Map(Map.Orientation.Orthogonal, 0, 0, 32, 32) map.setProperty("background_index", QString.number(backgroundIndex)) # Load our Tilesets. typeTilesets = QVector() tileIndexTilesets = QVector() self.loadTilesetsFromResources(map, typeTilesets, tileIndexTilesets) # Load each of our layers. for i in range(layerCount): # Parse layer header. _type = _in.readUInt8() tileIndex = _in.readUInt8() scrollSpeed = _in.readFloat() levelSignature = _in.readUInt8() width = _in.readUInt32() height = _in.readUInt32() if (_in.status() == QDataStream.ReadPastEnd or levelSignature != 42): self.mError = self.tr("Can't parse layer header!") return 0 # Make sure our width and height are consistent. if (map.width() == 0): map.setWidth(width) if (map.height() == 0): map.setHeight(height) if (map.width() != width or map.height() != height): self.mError = self.tr("Inconsistent layer sizes!") return 0 # Create a layer object. layer = TileLayer(self.layerTypeToName(_type), 0, 0, width, height) layer.setProperty("type", QString.number(_type)) layer.setProperty("tile_index", QString.number(tileIndex)) layer.setProperty("scroll_speed", QString.number(scrollSpeed, 'f')) map.addLayer(layer) # Look up the tileset for this layer. tileset = tilesetForLayer(_type, tileIndex, typeTilesets, tileIndexTilesets) # Read our tile data all at once. #tileData = QByteArray(width*height, b'\x00') bytesNeeded = width * height tileData = _in.readRawData(bytesNeeded) bytesRead = len(tileData) if (bytesRead != bytesNeeded): self.mError = self.tr("File ended in middle of layer!") return 0 i = 0 # Add the tiles to our layer. for y in range(0, height): for x in range(0, width): tile_id = tileData[i] & 0xff i += 1 if (tile_id != 255): tile = tileset.tileAt(tile_id) layer.setCell(x, y, Cell(tile)) # Make sure we read the entire *.bin file. if (_in.status() != QDataStream.Ok or not _in.atEnd()): self.mError = self.tr("Unexpected data at end of file!") return 0 return map