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 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 addTileLayer(self, layerdef, creditVisibility=True): """@api @param layerdef - an object of TileLayerDefinition class (in tiles.py) @param creditVisibility - visibility of credit label @returns newly created tile layer. if the layer is invalid, returns None @note added in 0.60 """ if self.crs3857 is None: self.crs3857 = QgsCoordinateReferenceSystem(3857) layer = TileLayer(self, layerdef, creditVisibility) if not layer.isValid(): return None QgsMapLayerRegistry.instance().addMapLayer(layer) self.layers[layer.id()] = layer # Action for saving tiles layer.saveTilesAction = QAction(self.tr('Save tiles'), self.iface.legendInterface()) self.iface.legendInterface().addLegendLayerAction(layer.saveTilesAction, "", u"savetiles", QgsMapLayer.PluginLayer, False) self.iface.legendInterface().addLegendLayerActionForLayer(layer.saveTilesAction, layer) layer.saveTilesAction.triggered.connect(layer.saveTiles) return layer
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 addTileLayer(self, layerdef, creditVisibility=True): """@api @param layerdef - an object of TileLayerDefinition class (in tiles.py) @param creditVisibility - visibility of credit label @returns newly created tile layer. if the layer is invalid, returns None @note added in 0.60 """ if self.crs3857 is None: self.crs3857 = QgsCoordinateReferenceSystem(3857) layer = TileLayer(self, layerdef, creditVisibility) if not layer.isValid(): return None QgsMapLayerRegistry.instance().addMapLayer(layer) self.layers[layer.id()] = layer return layer
def __readLayer(self): atts = self.xml.attributes() name = atts.value("name") x = Int(atts.value("x")) y = Int(atts.value("y")) width = Int(atts.value("width")) height = Int(atts.value("height")) tileLayer = TileLayer(name, x, y, width, height) readLayerAttributes(tileLayer, atts) while (self.xml.readNextStartElement()): if (self.xml.name() == "properties"): tileLayer.mergeProperties(self.__readProperties()) elif (self.xml.name() == "data"): self.__readLayerData(tileLayer) else: self.__readUnknownElement() return tileLayer
def addTileLayer(self, layerdef, creditVisibility=True): """@api @param layerdef - an object of TileLayerDefinition class (in tiles.py) @param creditVisibility - visibility of credit label @returns newly created tile layer. if the layer is invalid, returns None @note added in 0.60 """ if self.crs3857 is None: self.crs3857 = QgsCoordinateReferenceSystem(3857) layer = TileLayer(self, layerdef, creditVisibility) if not layer.isValid(): return None QgsMapLayerRegistry.instance().addMapLayer(layer) self.layers[layer.id()] = layer self.addActionToLayer(layer) return layer
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 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 createMap(self): if (self.exec() != QDialog.Accepted): return None mapWidth = self.mUi.mapWidth.value() mapHeight = self.mUi.mapHeight.value() tileWidth = self.mUi.tileWidth.value() tileHeight = self.mUi.tileHeight.value() orientationIndex = self.mUi.orientation.currentIndex() orientationData = self.mUi.orientation.itemData(orientationIndex) orientation = orientationData layerFormat = Map.LayerDataFormat(self.mUi.layerFormat.currentIndex()) renderOrder = Map.RenderOrder(self.mUi.renderOrder.currentIndex()) map = Map(orientation, mapWidth, mapHeight, tileWidth, tileHeight) map.setLayerDataFormat(layerFormat) map.setRenderOrder(renderOrder) gigabyte = 1073741824 memory = mapWidth * mapHeight * 8#sizeof(Cell) # Add a tile layer to new maps of reasonable size if (memory < gigabyte): map.addLayer(TileLayer(self.tr("Tile Layer 1"), 0, 0, mapWidth, mapHeight)) else: gigabytes = memory / gigabyte QMessageBox.warning(self, self.tr("Memory Usage Warning"), self.tr("Tile layers for this map will consume %.2f GB " "of memory each. Not creating one by default."%gigabytes)) # Store settings for next time prefs = preferences.Preferences.instance() prefs.setLayerDataFormat(layerFormat) prefs.setMapRenderOrder(renderOrder) s = preferences.Preferences.instance().settings() s.setValue(ORIENTATION_KEY, orientationIndex) s.setValue(MAP_WIDTH_KEY, mapWidth) s.setValue(MAP_HEIGHT_KEY, mapHeight) s.setValue(TILE_WIDTH_KEY, tileWidth) s.setValue(TILE_HEIGHT_KEY, tileHeight) return MapDocument(map)
def addLayer(self, layerType): layer = None name = QString() x = layerType if x == Layer.TileLayerType: name = self.tr("Tile Layer %d" % (self.mMap.tileLayerCount() + 1)) layer = TileLayer(name, 0, 0, self.mMap.width(), self.mMap.height()) elif x == Layer.ObjectGroupType: name = self.tr("Object Layer %d" % (self.mMap.objectGroupCount() + 1)) layer = ObjectGroup(name, 0, 0, self.mMap.width(), self.mMap.height()) elif x == Layer.ImageLayerType: name = self.tr("Image Layer %d" % (self.mMap.imageLayerCount() + 1)) layer = ImageLayer(name, 0, 0, self.mMap.width(), self.mMap.height()) index = self.mMap.layerCount() self.mUndoStack.push(AddLayer(self, index, layer)) self.setCurrentLayerIndex(index) self.editLayerNameRequested.emit()
def setupMissingLayers(self): # make sure all needed layers are there: for name in self.mTouchedTileLayers: if (self.mMapWork.indexOfLayer(name, Layer.TileLayerType) != -1): continue index = self.mMapWork.layerCount() tilelayer = TileLayer(name, 0, 0, self.mMapWork.width(), self.mMapWork.height()) self.mMapDocument.undoStack().push( AddLayer(self.mMapDocument, index, tilelayer)) self.mAddedTileLayers.append(name) for name in self.mTouchedObjectGroups: if (self.mMapWork.indexOfLayer(name, Layer.ObjectGroupType) != -1): continue index = self.mMapWork.layerCount() objectGroup = ObjectGroup(name, 0, 0, self.mMapWork.width(), self.mMapWork.height()) self.mMapDocument.undoStack().push( AddLayer(self.mMapDocument, index, objectGroup)) self.mAddedTileLayers.append(name) return True
def read(self, fileName): file = QFile(fileName) if (not file.open (QIODevice.ReadOnly)): self.mError = self.tr("Could not open file for reading.") return None # default to values of the original flare alpha game. map = Map(Map.Orientation.Isometric, 256, 256, 64, 32) stream = QTextStream(file) line = QString() sectionName = QString() newsection = False path = QFileInfo(file).absolutePath() base = 10 gidMapper = GidMapper() gid = 1 tilelayer = None objectgroup = None mapobject = None tilesetsSectionFound = False headerSectionFound = False tilelayerSectionFound = False # tile layer or objects while (not stream.atEnd()): line = stream.readLine() if line == '': continue startsWith = line[0] if (startsWith == '['): sectionName = line[1:line.index(']')] newsection = True continue if (sectionName == "header"): headerSectionFound = True #get map properties epos = line.index('=') if (epos != -1): key = line[:epos].strip() value = line[epos + 1:].strip() if (key == "width"): map.setWidth(Int(value)) elif (key == "height"): map.setHeight(Int(value)) elif (key == "tilewidth"): map.setTileWidth(Int(value)) elif (key == "tileheight"): map.setTileHeight(Int(value)) elif (key == "orientation"): map.setOrientation(orientationFromString(value)) else: map.setProperty(key, value) elif (sectionName == "tilesets"): tilesetsSectionFound = True epos = line.index('=') key = line[:epos].strip() value = line[epos + 1:].strip() if (key == "tileset"): _list = value.split(',') absoluteSource = _list[0] if (QDir.isRelativePath(absoluteSource)): absoluteSource = path + '/' + absoluteSource tilesetwidth = 0 tilesetheight = 0 if len(_list) > 2: tilesetwidth = Int(_list[1]) tilesetheight = Int(_list[2]) tileset = Tileset.create(QFileInfo(absoluteSource).fileName(), tilesetwidth, tilesetheight) ok = tileset.loadFromImage(absoluteSource) if not ok: self.mError = self.tr("Error loading tileset %s, which expands to %s. Path not found!"%(_list[0], absoluteSource)) return None else : if len(_list) > 4: tileset.setTileOffset(QPoint(Int(_list[3]),Int(_list[4]))) gidMapper.insert(gid, tileset) if len(_list) > 5: gid += Int(_list[5]) else : gid += tileset.tileCount() map.addTileset(tileset) elif (sectionName == "layer"): if (not tilesetsSectionFound): self.mError = self.tr("No tilesets section found before layer section.") return None tilelayerSectionFound = True epos = line.index('=') if (epos != -1): key = line[:epos].strip() value = line[epos + 1:].strip() if (key == "type"): tilelayer = TileLayer(value, 0, 0, map.width(),map.height()) map.addLayer(tilelayer) elif (key == "format"): if (value == "dec"): base = 10 elif (value == "hex"): base = 16 elif (key == "data"): for y in range(map.height()): line = stream.readLine() l = line.split(',') for x in range(min(map.width(), len(l))): ok = False tileid = int(l[x], base) c, ok = gidMapper.gidToCell(tileid) if (not ok): self.mError += self.tr("Error mapping tile id %1.").arg(tileid) return None tilelayer.setCell(x, y, c) else : tilelayer.setProperty(key, value) else : if (newsection): if (map.indexOfLayer(sectionName) == -1): objectgroup = ObjectGroup(sectionName, 0,0,map.width(), map.height()) map.addLayer(objectgroup) else : objectgroup = map.layerAt(map.indexOfLayer(sectionName)) mapobject = MapObject() objectgroup.addObject(mapobject) newsection = False if (not mapobject): continue if (startsWith == '#'): name = line[1].strip() mapobject.setName(name) epos = line.index('=') if (epos != -1): key = line[:epos].strip() value = line[epos + 1:].strip() if (key == "type"): mapobject.setType(value) elif (key == "location"): loc = value.split(',') x,y = 0.0, 0.0 w,h = 0, 0 if (map.orientation() == Map.Orthogonal): x = loc[0].toFloat()*map.tileWidth() y = loc[1].toFloat()*map.tileHeight() if len(loc) > 3: w = Int(loc[2])*map.tileWidth() h = Int(loc[3])*map.tileHeight() else : w = map.tileWidth() h = map.tileHeight() else : x = loc[0].toFloat()*map.tileHeight() y = loc[1].toFloat()*map.tileHeight() if len(loc) > 3: w = Int(loc[2])*map.tileHeight() h = Int(loc[3])*map.tileHeight() else : w = h = map.tileHeight() mapobject.setPosition(QPointF(x, y)) mapobject.setSize(w, h) else : mapobject.setProperty(key, value) if (not headerSectionFound or not tilesetsSectionFound or not tilelayerSectionFound): self.mError = self.tr("This seems to be no valid flare map. " "A Flare map consists of at least a header " "section, a tileset section and one tile layer.") return None return map
def toTileLayer(self, variantMap): name = variantMap.get("name",'') width = variantMap.get("width",0) height = variantMap.get("height",0) dataVariant = variantMap["data"] tileLayer = TileLayer(name, variantMap.get("x",0), variantMap.get("y",0), width, height) opacity = variantMap.get("opacity", 0.0) visible = variantMap.get("visible", True) tileLayer.setOpacity(opacity) tileLayer.setVisible(visible) encoding = variantMap.get("encoding", '') compression = variantMap.get("compression", '') if encoding=='' or encoding == "csv": layerDataFormat = Map.LayerDataFormat.CSV elif (encoding == "base64"): if compression=='': layerDataFormat = Map.LayerDataFormat.Base64 elif (compression == "gzip"): layerDataFormat = Map.LayerDataFormat.Base64Gzip elif (compression == "zlib"): layerDataFormat = Map.LayerDataFormat.Base64Zlib else: self.mError = self.tr("Compression method '%s' not supported"%compression) return None else: self.mError = self.tr("Unknown encoding: %1"%encoding) return None self.mMap.setLayerDataFormat(layerDataFormat) if layerDataFormat==Map.LayerDataFormat.XML or layerDataFormat==Map.LayerDataFormat.CSV: dataVariantList = dataVariant if len(dataVariantList) != width * height: self.mError = self.tr("Corrupt layer data for layer '%s'"%name) return None x = 0 y = 0 ok = False if (len(dataVariantList) != width * height): self.mError = self.tr("Corrupt layer data for layer '%s'"%name) return None for gidVariant in dataVariantList: gid, ok = Int2(gidVariant) if (not ok): self.mError = self.tr("Unable to parse tile at (%d,%d) on layer '%s'"%(x, y, tileLayer.name())) return None cell = self.mGidMapper.gidToCell(gid, ok) tileLayer.setCell(x, y, cell) x += 1 if (x >= tileLayer.width()): x = 0 y += 1 elif layerDataFormat==Map.LayerDataFormat.Base64 \ or layerDataFormat==Map.LayerDataFormat.Base64Zlib \ or layerDataFormat==Map.LayerDataFormat.Base64Gzip: data = QByteArray(dataVariant) error = self.mGidMapper.decodeLayerData(tileLayer, data, layerDataFormat) if error==DecodeError.CorruptLayerData: self.mError = self.tr("Corrupt layer data for layer '%s'"%name) return None elif error==DecodeError.TileButNoTilesets: self.mError = self.tr("Tile used but no tilesets specified") return None elif error==DecodeError.InvalidTile: self.mError = self.tr("Invalid tile: %d"%self.mGidMapper.invalidTile()) return None elif error==DecodeError.NoError: pass return tileLayer
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 BucketFillTool(AbstractTileTool): def tr(self, sourceText, disambiguation = '', n = -1): return QCoreApplication.translate('BucketFillTool', sourceText, disambiguation, n) def __init__(self, parent = None): super().__init__(self.tr("Bucket Fill Tool"), QIcon(":images/22x22/stock-tool-bucket-fill.png"), QKeySequence(self.tr("F")), parent) self.mStamp = TileStamp() self.mFillOverlay = None self.mFillRegion = QRegion() self.mMissingTilesets = QVector() self.mIsActive = False self.mLastShiftStatus = False ## # Indicates if the tool is using the random mode. ## self.mIsRandom = False ## # Contains the value of mIsRandom at that time, when the latest call of # tilePositionChanged() took place. # This variable is needed to detect if the random mode was changed during # mFillOverlay being brushed at an area. ## self.mLastRandomStatus = False ## # Contains all used random cells to use in random mode. # The same cell can be in the list multiple times to make different # random weights possible. ## self.mRandomCellPicker = RandomPicker() def __del__(self): pass def activate(self, scene): super().activate(scene) self.mIsActive = True self.tilePositionChanged(self.tilePosition()) def deactivate(self, scene): super().deactivate(scene) self.mFillRegion = QRegion() self.mIsActive = False def mousePressed(self, event): if (event.button() != Qt.LeftButton or self.mFillRegion.isEmpty()): return if (not self.brushItem().isVisible()): return preview = self.mFillOverlay if not preview: return paint = PaintTileLayer(self.mapDocument(), self.currentTileLayer(), preview.x(), preview.y(), preview) paint.setText(QCoreApplication.translate("Undo Commands", "Fill Area")) if not self.mMissingTilesets.isEmpty(): for tileset in self.mMissingTilesets: AddTileset(self.mapDocument(), tileset, paint) self.mMissingTilesets.clear() fillRegion = QRegion(self.mFillRegion) self.mapDocument().undoStack().push(paint) self.mapDocument().emitRegionEdited(fillRegion, self.currentTileLayer()) def mouseReleased(self, event): pass def modifiersChanged(self, modifiers): # Don't need to recalculate fill region if there was no fill region if (not self.mFillOverlay): return self.tilePositionChanged(self.tilePosition()) def languageChanged(self): self.setName(self.tr("Bucket Fill Tool")) self.setShortcut(QKeySequence(self.tr("F"))) ## # Sets the stamp that is drawn when filling. The BucketFillTool takes # ownership over the stamp layer. ## def setStamp(self, stamp): # Clear any overlay that we presently have with an old stamp self.clearOverlay() self.mStamp = stamp self.updateRandomListAndMissingTilesets() if (self.mIsActive and self.brushItem().isVisible()): self.tilePositionChanged(self.tilePosition()) ## # This returns the actual tile layer which is used to define the current # state. ## def stamp(self): return TileStamp(self.mStamp) def setRandom(self, value): if (self.mIsRandom == value): return self.mIsRandom = value self.updateRandomListAndMissingTilesets() # Don't need to recalculate fill region if there was no fill region if (not self.mFillOverlay): return self.tilePositionChanged(self.tilePosition()) def tilePositionChanged(self, tilePos): # Skip filling if the stamp is empty if self.mStamp.isEmpty(): return # Make sure that a tile layer is selected tileLayer = self.currentTileLayer() if (not tileLayer): return shiftPressed = QApplication.keyboardModifiers() & Qt.ShiftModifier fillRegionChanged = False regionComputer = TilePainter(self.mapDocument(), tileLayer) # If the stamp is a single tile, ignore it when making the region if (not shiftPressed and self.mStamp.variations().size() == 1): variation = self.mStamp.variations().first() stampLayer = variation.tileLayer() if (stampLayer.size() == QSize(1, 1) and stampLayer.cellAt(0, 0) == regionComputer.cellAt(tilePos)): return # This clears the connections so we don't get callbacks self.clearConnections(self.mapDocument()) # Optimization: we don't need to recalculate the fill area # if the new mouse position is still over the filled region # and the shift modifier hasn't changed. if (not self.mFillRegion.contains(tilePos) or shiftPressed != self.mLastShiftStatus): # Clear overlay to make way for a new one self.clearOverlay() # Cache information about how the fill region was created self.mLastShiftStatus = shiftPressed # Get the new fill region if (not shiftPressed): # If not holding shift, a region is generated from the current pos self.mFillRegion = regionComputer.computePaintableFillRegion(tilePos) else: # If holding shift, the region is the selection bounds self.mFillRegion = self.mapDocument().selectedArea() # Fill region is the whole map if there is no selection if (self.mFillRegion.isEmpty()): self.mFillRegion = tileLayer.bounds() # The mouse needs to be in the region if (not self.mFillRegion.contains(tilePos)): self.mFillRegion = QRegion() fillRegionChanged = True # Ensure that a fill region was created before making an overlay layer if (self.mFillRegion.isEmpty()): return if (self.mLastRandomStatus != self.mIsRandom): self.mLastRandomStatus = self.mIsRandom fillRegionChanged = True if (not self.mFillOverlay): # Create a new overlay region fillBounds = self.mFillRegion.boundingRect() self.mFillOverlay = TileLayer(QString(), fillBounds.x(), fillBounds.y(), fillBounds.width(), fillBounds.height()) # Paint the new overlay if (not self.mIsRandom): if (fillRegionChanged or self.mStamp.variations().size() > 1): fillWithStamp(self.mFillOverlay, self.mStamp, self.mFillRegion.translated(-self.mFillOverlay.position())) fillRegionChanged = True else: self.randomFill(self.mFillOverlay, self.mFillRegion) fillRegionChanged = True if (fillRegionChanged): # Update the brush item to draw the overlay self.brushItem().setTileLayer(self.mFillOverlay) # Create connections to know when the overlay should be cleared self.makeConnections() def mapDocumentChanged(self, oldDocument, newDocument): super().mapDocumentChanged(oldDocument, newDocument) self.clearConnections(oldDocument) # Reset things that are probably invalid now if newDocument: self.updateRandomListAndMissingTilesets() self.clearOverlay() def clearOverlay(self): # Clear connections before clearing overlay so there is no # risk of getting a callback and causing an infinite loop self.clearConnections(self.mapDocument()) self.brushItem().clear() self.mFillOverlay = None self.mFillRegion = QRegion() def makeConnections(self): if (not self.mapDocument()): return # Overlay may need to be cleared if a region changed self.mapDocument().regionChanged.connect(self.clearOverlay) # Overlay needs to be cleared if we switch to another layer self.mapDocument().currentLayerIndexChanged.connect(self.clearOverlay) # Overlay needs be cleared if the selection changes, since # the overlay may be bound or may need to be bound to the selection self.mapDocument().selectedAreaChanged.connect(self.clearOverlay) def clearConnections(self, mapDocument): if (not mapDocument): return try: mapDocument.regionChanged.disconnect(self.clearOverlay) mapDocument.currentLayerIndexChanged.disconnect(self.clearOverlay) mapDocument.selectedAreaChanged.disconnect(self.clearOverlay) except: pass ## # Updates the list of random cells. # This is done by taking all non-null tiles from the original stamp mStamp. ## def updateRandomListAndMissingTilesets(self): self.mRandomCellPicker.clear() self.mMissingTilesets.clear() for variation in self.mStamp.variations(): self.mapDocument().unifyTilesets(variation.map, self.mMissingTilesets) if self.mIsRandom: for cell in variation.tileLayer(): if not cell.isEmpty(): self.mRandomCellPicker.add(cell, cell.tile.probability()) ## # Fills the given \a region in the given \a tileLayer with random tiles. ## def randomFill(self, tileLayer, region): if (region.isEmpty() or self.mRandomList.empty()): return for rect in region.translated(-tileLayer.position()).rects(): for _x in range(rect.left(), rect.right()+1): for _y in range(rect.top(), rect.bottom()+1): tileLayer.setCell(_x, _y, self.mRandomCellPicker.pick())
def tilePositionChanged(self, tilePos): # Skip filling if the stamp is empty if self.mStamp.isEmpty(): return # Make sure that a tile layer is selected tileLayer = self.currentTileLayer() if (not tileLayer): return shiftPressed = QApplication.keyboardModifiers() & Qt.ShiftModifier fillRegionChanged = False regionComputer = TilePainter(self.mapDocument(), tileLayer) # If the stamp is a single tile, ignore it when making the region if (not shiftPressed and self.mStamp.variations().size() == 1): variation = self.mStamp.variations().first() stampLayer = variation.tileLayer() if (stampLayer.size() == QSize(1, 1) and stampLayer.cellAt(0, 0) == regionComputer.cellAt(tilePos)): return # This clears the connections so we don't get callbacks self.clearConnections(self.mapDocument()) # Optimization: we don't need to recalculate the fill area # if the new mouse position is still over the filled region # and the shift modifier hasn't changed. if (not self.mFillRegion.contains(tilePos) or shiftPressed != self.mLastShiftStatus): # Clear overlay to make way for a new one self.clearOverlay() # Cache information about how the fill region was created self.mLastShiftStatus = shiftPressed # Get the new fill region if (not shiftPressed): # If not holding shift, a region is generated from the current pos self.mFillRegion = regionComputer.computePaintableFillRegion(tilePos) else: # If holding shift, the region is the selection bounds self.mFillRegion = self.mapDocument().selectedArea() # Fill region is the whole map if there is no selection if (self.mFillRegion.isEmpty()): self.mFillRegion = tileLayer.bounds() # The mouse needs to be in the region if (not self.mFillRegion.contains(tilePos)): self.mFillRegion = QRegion() fillRegionChanged = True # Ensure that a fill region was created before making an overlay layer if (self.mFillRegion.isEmpty()): return if (self.mLastRandomStatus != self.mIsRandom): self.mLastRandomStatus = self.mIsRandom fillRegionChanged = True if (not self.mFillOverlay): # Create a new overlay region fillBounds = self.mFillRegion.boundingRect() self.mFillOverlay = TileLayer(QString(), fillBounds.x(), fillBounds.y(), fillBounds.width(), fillBounds.height()) # Paint the new overlay if (not self.mIsRandom): if (fillRegionChanged or self.mStamp.variations().size() > 1): fillWithStamp(self.mFillOverlay, self.mStamp, self.mFillRegion.translated(-self.mFillOverlay.position())) fillRegionChanged = True else: self.randomFill(self.mFillOverlay, self.mFillRegion) fillRegionChanged = True if (fillRegionChanged): # Update the brush item to draw the overlay self.brushItem().setTileLayer(self.mFillOverlay) # Create connections to know when the overlay should be cleared self.makeConnections()
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()
def toTileLayer(self, variantMap): name = variantMap.get("name", '') width = variantMap.get("width", 0) height = variantMap.get("height", 0) dataVariant = variantMap["data"] tileLayer = TileLayer(name, variantMap.get("x", 0), variantMap.get("y", 0), width, height) opacity = variantMap.get("opacity", 0.0) visible = variantMap.get("visible", True) tileLayer.setOpacity(opacity) tileLayer.setVisible(visible) encoding = variantMap.get("encoding", '') compression = variantMap.get("compression", '') if encoding == '' or encoding == "csv": layerDataFormat = Map.LayerDataFormat.CSV elif (encoding == "base64"): if compression == '': layerDataFormat = Map.LayerDataFormat.Base64 elif (compression == "gzip"): layerDataFormat = Map.LayerDataFormat.Base64Gzip elif (compression == "zlib"): layerDataFormat = Map.LayerDataFormat.Base64Zlib else: self.mError = self.tr("Compression method '%s' not supported" % compression) return None else: self.mError = self.tr("Unknown encoding: %1" % encoding) return None self.mMap.setLayerDataFormat(layerDataFormat) if layerDataFormat == Map.LayerDataFormat.XML or layerDataFormat == Map.LayerDataFormat.CSV: dataVariantList = dataVariant if len(dataVariantList) != width * height: self.mError = self.tr("Corrupt layer data for layer '%s'" % name) return None x = 0 y = 0 ok = False if (len(dataVariantList) != width * height): self.mError = self.tr("Corrupt layer data for layer '%s'" % name) return None for gidVariant in dataVariantList: gid, ok = Int2(gidVariant) if (not ok): self.mError = self.tr( "Unable to parse tile at (%d,%d) on layer '%s'" % (x, y, tileLayer.name())) return None cell = self.mGidMapper.gidToCell(gid, ok) tileLayer.setCell(x, y, cell) x += 1 if (x >= tileLayer.width()): x = 0 y += 1 elif layerDataFormat==Map.LayerDataFormat.Base64 \ or layerDataFormat==Map.LayerDataFormat.Base64Zlib \ or layerDataFormat==Map.LayerDataFormat.Base64Gzip: data = QByteArray(dataVariant) error = self.mGidMapper.decodeLayerData(tileLayer, data, layerDataFormat) if error == DecodeError.CorruptLayerData: self.mError = self.tr("Corrupt layer data for layer '%s'" % name) return None elif error == DecodeError.TileButNoTilesets: self.mError = self.tr("Tile used but no tilesets specified") return None elif error == DecodeError.InvalidTile: self.mError = self.tr("Invalid tile: %d" % self.mGidMapper.invalidTile()) return None elif error == DecodeError.NoError: pass return tileLayer
def read(self, fileName): file = QFile(fileName) if (not file.open(QIODevice.ReadOnly)): self.mError = self.tr("Could not open file for reading.") return None # default to values of the original flare alpha game. map = Map(Map.Orientation.Isometric, 256, 256, 64, 32) stream = QTextStream(file) line = QString() sectionName = QString() newsection = False path = QFileInfo(file).absolutePath() base = 10 gidMapper = GidMapper() gid = 1 tilelayer = None objectgroup = None mapobject = None tilesetsSectionFound = False headerSectionFound = False tilelayerSectionFound = False # tile layer or objects while (not stream.atEnd()): line = stream.readLine() if line == '': continue startsWith = line[0] if (startsWith == '['): sectionName = line[1:line.index(']')] newsection = True continue if (sectionName == "header"): headerSectionFound = True #get map properties epos = line.index('=') if (epos != -1): key = line[:epos].strip() value = line[epos + 1:].strip() if (key == "width"): map.setWidth(Int(value)) elif (key == "height"): map.setHeight(Int(value)) elif (key == "tilewidth"): map.setTileWidth(Int(value)) elif (key == "tileheight"): map.setTileHeight(Int(value)) elif (key == "orientation"): map.setOrientation(orientationFromString(value)) else: map.setProperty(key, value) elif (sectionName == "tilesets"): tilesetsSectionFound = True epos = line.index('=') key = line[:epos].strip() value = line[epos + 1:].strip() if (key == "tileset"): _list = value.split(',') absoluteSource = _list[0] if (QDir.isRelativePath(absoluteSource)): absoluteSource = path + '/' + absoluteSource tilesetwidth = 0 tilesetheight = 0 if len(_list) > 2: tilesetwidth = Int(_list[1]) tilesetheight = Int(_list[2]) tileset = Tileset.create( QFileInfo(absoluteSource).fileName(), tilesetwidth, tilesetheight) ok = tileset.loadFromImage(absoluteSource) if not ok: self.mError = self.tr( "Error loading tileset %s, which expands to %s. Path not found!" % (_list[0], absoluteSource)) return None else: if len(_list) > 4: tileset.setTileOffset( QPoint(Int(_list[3]), Int(_list[4]))) gidMapper.insert(gid, tileset) if len(_list) > 5: gid += Int(_list[5]) else: gid += tileset.tileCount() map.addTileset(tileset) elif (sectionName == "layer"): if (not tilesetsSectionFound): self.mError = self.tr( "No tilesets section found before layer section.") return None tilelayerSectionFound = True epos = line.index('=') if (epos != -1): key = line[:epos].strip() value = line[epos + 1:].strip() if (key == "type"): tilelayer = TileLayer(value, 0, 0, map.width(), map.height()) map.addLayer(tilelayer) elif (key == "format"): if (value == "dec"): base = 10 elif (value == "hex"): base = 16 elif (key == "data"): for y in range(map.height()): line = stream.readLine() l = line.split(',') for x in range(min(map.width(), len(l))): ok = False tileid = int(l[x], base) c, ok = gidMapper.gidToCell(tileid) if (not ok): self.mError += self.tr( "Error mapping tile id %1.").arg( tileid) return None tilelayer.setCell(x, y, c) else: tilelayer.setProperty(key, value) else: if (newsection): if (map.indexOfLayer(sectionName) == -1): objectgroup = ObjectGroup(sectionName, 0, 0, map.width(), map.height()) map.addLayer(objectgroup) else: objectgroup = map.layerAt( map.indexOfLayer(sectionName)) mapobject = MapObject() objectgroup.addObject(mapobject) newsection = False if (not mapobject): continue if (startsWith == '#'): name = line[1].strip() mapobject.setName(name) epos = line.index('=') if (epos != -1): key = line[:epos].strip() value = line[epos + 1:].strip() if (key == "type"): mapobject.setType(value) elif (key == "location"): loc = value.split(',') x, y = 0.0, 0.0 w, h = 0, 0 if (map.orientation() == Map.Orthogonal): x = loc[0].toFloat() * map.tileWidth() y = loc[1].toFloat() * map.tileHeight() if len(loc) > 3: w = Int(loc[2]) * map.tileWidth() h = Int(loc[3]) * map.tileHeight() else: w = map.tileWidth() h = map.tileHeight() else: x = loc[0].toFloat() * map.tileHeight() y = loc[1].toFloat() * map.tileHeight() if len(loc) > 3: w = Int(loc[2]) * map.tileHeight() h = Int(loc[3]) * map.tileHeight() else: w = h = map.tileHeight() mapobject.setPosition(QPointF(x, y)) mapobject.setSize(w, h) else: mapobject.setProperty(key, value) if (not headerSectionFound or not tilesetsSectionFound or not tilelayerSectionFound): self.mError = self.tr( "This seems to be no valid flare map. " "A Flare map consists of at least a header " "section, a tileset section and one tile layer.") return None return map
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
class BucketFillTool(AbstractTileTool): def tr(self, sourceText, disambiguation='', n=-1): return QCoreApplication.translate('BucketFillTool', sourceText, disambiguation, n) def __init__(self, parent=None): super().__init__(self.tr("Bucket Fill Tool"), QIcon(":images/22x22/stock-tool-bucket-fill.png"), QKeySequence(self.tr("F")), parent) self.mStamp = TileStamp() self.mFillOverlay = None self.mFillRegion = QRegion() self.mMissingTilesets = QVector() self.mIsActive = False self.mLastShiftStatus = False ## # Indicates if the tool is using the random mode. ## self.mIsRandom = False ## # Contains the value of mIsRandom at that time, when the latest call of # tilePositionChanged() took place. # This variable is needed to detect if the random mode was changed during # mFillOverlay being brushed at an area. ## self.mLastRandomStatus = False ## # Contains all used random cells to use in random mode. # The same cell can be in the list multiple times to make different # random weights possible. ## self.mRandomCellPicker = RandomPicker() def __del__(self): pass def activate(self, scene): super().activate(scene) self.mIsActive = True self.tilePositionChanged(self.tilePosition()) def deactivate(self, scene): super().deactivate(scene) self.mFillRegion = QRegion() self.mIsActive = False def mousePressed(self, event): if (event.button() != Qt.LeftButton or self.mFillRegion.isEmpty()): return if (not self.brushItem().isVisible()): return preview = self.mFillOverlay if not preview: return paint = PaintTileLayer(self.mapDocument(), self.currentTileLayer(), preview.x(), preview.y(), preview) paint.setText(QCoreApplication.translate("Undo Commands", "Fill Area")) if not self.mMissingTilesets.isEmpty(): for tileset in self.mMissingTilesets: AddTileset(self.mapDocument(), tileset, paint) self.mMissingTilesets.clear() fillRegion = QRegion(self.mFillRegion) self.mapDocument().undoStack().push(paint) self.mapDocument().emitRegionEdited(fillRegion, self.currentTileLayer()) def mouseReleased(self, event): pass def modifiersChanged(self, modifiers): # Don't need to recalculate fill region if there was no fill region if (not self.mFillOverlay): return self.tilePositionChanged(self.tilePosition()) def languageChanged(self): self.setName(self.tr("Bucket Fill Tool")) self.setShortcut(QKeySequence(self.tr("F"))) ## # Sets the stamp that is drawn when filling. The BucketFillTool takes # ownership over the stamp layer. ## def setStamp(self, stamp): # Clear any overlay that we presently have with an old stamp self.clearOverlay() self.mStamp = stamp self.updateRandomListAndMissingTilesets() if (self.mIsActive and self.brushItem().isVisible()): self.tilePositionChanged(self.tilePosition()) ## # This returns the actual tile layer which is used to define the current # state. ## def stamp(self): return TileStamp(self.mStamp) def setRandom(self, value): if (self.mIsRandom == value): return self.mIsRandom = value self.updateRandomListAndMissingTilesets() # Don't need to recalculate fill region if there was no fill region if (not self.mFillOverlay): return self.tilePositionChanged(self.tilePosition()) def tilePositionChanged(self, tilePos): # Skip filling if the stamp is empty if self.mStamp.isEmpty(): return # Make sure that a tile layer is selected tileLayer = self.currentTileLayer() if (not tileLayer): return shiftPressed = QApplication.keyboardModifiers() & Qt.ShiftModifier fillRegionChanged = False regionComputer = TilePainter(self.mapDocument(), tileLayer) # If the stamp is a single tile, ignore it when making the region if (not shiftPressed and self.mStamp.variations().size() == 1): variation = self.mStamp.variations().first() stampLayer = variation.tileLayer() if (stampLayer.size() == QSize(1, 1) and stampLayer.cellAt(0, 0) == regionComputer.cellAt(tilePos)): return # This clears the connections so we don't get callbacks self.clearConnections(self.mapDocument()) # Optimization: we don't need to recalculate the fill area # if the new mouse position is still over the filled region # and the shift modifier hasn't changed. if (not self.mFillRegion.contains(tilePos) or shiftPressed != self.mLastShiftStatus): # Clear overlay to make way for a new one self.clearOverlay() # Cache information about how the fill region was created self.mLastShiftStatus = shiftPressed # Get the new fill region if (not shiftPressed): # If not holding shift, a region is generated from the current pos self.mFillRegion = regionComputer.computePaintableFillRegion( tilePos) else: # If holding shift, the region is the selection bounds self.mFillRegion = self.mapDocument().selectedArea() # Fill region is the whole map if there is no selection if (self.mFillRegion.isEmpty()): self.mFillRegion = tileLayer.bounds() # The mouse needs to be in the region if (not self.mFillRegion.contains(tilePos)): self.mFillRegion = QRegion() fillRegionChanged = True # Ensure that a fill region was created before making an overlay layer if (self.mFillRegion.isEmpty()): return if (self.mLastRandomStatus != self.mIsRandom): self.mLastRandomStatus = self.mIsRandom fillRegionChanged = True if (not self.mFillOverlay): # Create a new overlay region fillBounds = self.mFillRegion.boundingRect() self.mFillOverlay = TileLayer(QString(), fillBounds.x(), fillBounds.y(), fillBounds.width(), fillBounds.height()) # Paint the new overlay if (not self.mIsRandom): if (fillRegionChanged or self.mStamp.variations().size() > 1): fillWithStamp( self.mFillOverlay, self.mStamp, self.mFillRegion.translated(-self.mFillOverlay.position())) fillRegionChanged = True else: self.randomFill(self.mFillOverlay, self.mFillRegion) fillRegionChanged = True if (fillRegionChanged): # Update the brush item to draw the overlay self.brushItem().setTileLayer(self.mFillOverlay) # Create connections to know when the overlay should be cleared self.makeConnections() def mapDocumentChanged(self, oldDocument, newDocument): super().mapDocumentChanged(oldDocument, newDocument) self.clearConnections(oldDocument) # Reset things that are probably invalid now if newDocument: self.updateRandomListAndMissingTilesets() self.clearOverlay() def clearOverlay(self): # Clear connections before clearing overlay so there is no # risk of getting a callback and causing an infinite loop self.clearConnections(self.mapDocument()) self.brushItem().clear() self.mFillOverlay = None self.mFillRegion = QRegion() def makeConnections(self): if (not self.mapDocument()): return # Overlay may need to be cleared if a region changed self.mapDocument().regionChanged.connect(self.clearOverlay) # Overlay needs to be cleared if we switch to another layer self.mapDocument().currentLayerIndexChanged.connect(self.clearOverlay) # Overlay needs be cleared if the selection changes, since # the overlay may be bound or may need to be bound to the selection self.mapDocument().selectedAreaChanged.connect(self.clearOverlay) def clearConnections(self, mapDocument): if (not mapDocument): return try: mapDocument.regionChanged.disconnect(self.clearOverlay) mapDocument.currentLayerIndexChanged.disconnect(self.clearOverlay) mapDocument.selectedAreaChanged.disconnect(self.clearOverlay) except: pass ## # Updates the list of random cells. # This is done by taking all non-null tiles from the original stamp mStamp. ## def updateRandomListAndMissingTilesets(self): self.mRandomCellPicker.clear() self.mMissingTilesets.clear() for variation in self.mStamp.variations(): self.mapDocument().unifyTilesets(variation.map, self.mMissingTilesets) if self.mIsRandom: for cell in variation.tileLayer(): if not cell.isEmpty(): self.mRandomCellPicker.add(cell, cell.tile.probability()) ## # Fills the given \a region in the given \a tileLayer with random tiles. ## def randomFill(self, tileLayer, region): if (region.isEmpty() or self.mRandomList.empty()): return for rect in region.translated(-tileLayer.position()).rects(): for _x in range(rect.left(), rect.right() + 1): for _y in range(rect.top(), rect.bottom() + 1): tileLayer.setCell(_x, _y, self.mRandomCellPicker.pick())
def tilePositionChanged(self, tilePos): # Skip filling if the stamp is empty if self.mStamp.isEmpty(): return # Make sure that a tile layer is selected tileLayer = self.currentTileLayer() if (not tileLayer): return shiftPressed = QApplication.keyboardModifiers() & Qt.ShiftModifier fillRegionChanged = False regionComputer = TilePainter(self.mapDocument(), tileLayer) # If the stamp is a single tile, ignore it when making the region if (not shiftPressed and self.mStamp.variations().size() == 1): variation = self.mStamp.variations().first() stampLayer = variation.tileLayer() if (stampLayer.size() == QSize(1, 1) and stampLayer.cellAt(0, 0) == regionComputer.cellAt(tilePos)): return # This clears the connections so we don't get callbacks self.clearConnections(self.mapDocument()) # Optimization: we don't need to recalculate the fill area # if the new mouse position is still over the filled region # and the shift modifier hasn't changed. if (not self.mFillRegion.contains(tilePos) or shiftPressed != self.mLastShiftStatus): # Clear overlay to make way for a new one self.clearOverlay() # Cache information about how the fill region was created self.mLastShiftStatus = shiftPressed # Get the new fill region if (not shiftPressed): # If not holding shift, a region is generated from the current pos self.mFillRegion = regionComputer.computePaintableFillRegion( tilePos) else: # If holding shift, the region is the selection bounds self.mFillRegion = self.mapDocument().selectedArea() # Fill region is the whole map if there is no selection if (self.mFillRegion.isEmpty()): self.mFillRegion = tileLayer.bounds() # The mouse needs to be in the region if (not self.mFillRegion.contains(tilePos)): self.mFillRegion = QRegion() fillRegionChanged = True # Ensure that a fill region was created before making an overlay layer if (self.mFillRegion.isEmpty()): return if (self.mLastRandomStatus != self.mIsRandom): self.mLastRandomStatus = self.mIsRandom fillRegionChanged = True if (not self.mFillOverlay): # Create a new overlay region fillBounds = self.mFillRegion.boundingRect() self.mFillOverlay = TileLayer(QString(), fillBounds.x(), fillBounds.y(), fillBounds.width(), fillBounds.height()) # Paint the new overlay if (not self.mIsRandom): if (fillRegionChanged or self.mStamp.variations().size() > 1): fillWithStamp( self.mFillOverlay, self.mStamp, self.mFillRegion.translated(-self.mFillOverlay.position())) fillRegionChanged = True else: self.randomFill(self.mFillOverlay, self.mFillRegion) fillRegionChanged = True if (fillRegionChanged): # Update the brush item to draw the overlay self.brushItem().setTileLayer(self.mFillOverlay) # Create connections to know when the overlay should be cleared self.makeConnections()
def drawPreviewLayer(self, _list): self.mPreviewLayer = None if self.mStamp.isEmpty(): return if self.mIsRandom: if self.mRandomCellPicker.isEmpty(): return paintedRegion = QRegion() for p in _list: paintedRegion += QRect(p, QSize(1, 1)) bounds = paintedRegion.boundingRect() preview = TileLayer(QString(), bounds.x(), bounds.y(), bounds.width(), bounds.height()) for p in _list: cell = self.mRandomCellPicker.pick() preview.setCell(p.x() - bounds.left(), p.y() - bounds.top(), cell) self.mPreviewLayer = preview else: self.mMissingTilesets.clear() paintedRegion = QRegion() operations = QVector() regionCache = QHash() for p in _list: variation = self.mStamp.randomVariation() self.mapDocument().unifyTilesets(variation.map, self.mMissingTilesets) stamp = variation.tileLayer() stampRegion = QRegion() if regionCache.contains(stamp): stampRegion = regionCache.value(stamp) else: stampRegion = stamp.region() regionCache.insert(stamp, stampRegion) centered = QPoint(p.x() - int(stamp.width() / 2), p.y() - int(stamp.height() / 2)) region = stampRegion.translated(centered.x(), centered.y()) if not paintedRegion.intersects(region): paintedRegion += region op = PaintOperation(centered, stamp) operations.append(op) bounds = paintedRegion.boundingRect() preview = TileLayer(QString(), bounds.x(), bounds.y(), bounds.width(), bounds.height()) for op in operations: preview.merge(op.pos - bounds.topLeft(), op.stamp) self.mPreviewLayer = preview
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