Example #1
0
class AnvilWorldAdapter(object):
    """
    Provides an interface to AnvilWorldFolder/RevisionHistory that is usable by WorldEditor

    This interface is the base used for all adapter classes. When writing a new adapter, make sure to
    implement all required methods and attributes. Required methods and attrs are the ones with docstrings.
    """

    minHeight = 0
    maxHeight = 256
    blocktypes = pc_blocktypes
    hasLights = True

    EntityRef = PCEntityRef
    TileEntityRef = PCTileEntityRef

    def __init__(self, filename=None, create=False, readonly=False, resume=None):
        """
        Load a Minecraft for PC level (Anvil format) from the given filename. It can point to either
        a level.dat or a folder containing one. If create is True, it will
        also create the world using a randomly selected seed.

        If you try to create an existing world, IOError will be raised.

        Uses a RevisionHistory to manage undo history. Upon creation, the world is read-only until createRevision() is
        called. Call createRevision() to create a new revision, or selectRevision() to revert to an earlier
        revision. Older revisions are read-only, so createRevision() must be called again to make further changes.

        Call writeAllChanges() to write all changes into the original world.

        :type filename: str or unicode
        :type create: bool
        :type readonly: bool
        :rtype: AnvilWorldAdapter
        """
        self.lockTime = 0

        assert not (create and readonly)

        if os.path.basename(filename) in ("level.dat", "level.dat_old"):
            filename = os.path.dirname(filename)

        if not os.path.exists(filename):
            if not create:
                raise IOError('File not found')

            os.mkdir(filename)
        else:
            if create:
                if not os.path.isdir(filename) or os.path.exists(os.path.join(filename, "level.dat")):
                    raise IOError('File exists!')

        if not os.path.isdir(filename):
            raise IOError('File is not a Minecraft Anvil world')

        if readonly:
            self.revisionHistory = AnvilWorldFolder(filename)
            self.selectedRevision = self.revisionHistory
        else:
            self.revisionHistory = RevisionHistory(filename, resume)
            self.selectedRevision = self.revisionHistory.getHead()

        self.filename = filename
        self.readonly = readonly
        if not readonly:
            self.acquireSessionLock()

        if create:
            self._createMetadataTag()
            self.selectedRevision.writeFile("level.dat", self.metadata.metadataTag.save())

        else:
            self.loadMetadata()

    def __repr__(self):
        return "AnvilWorldAdapter(%r)" % self.filename

    # --- Create, save, close ---

    def loadMetadata(self):
        try:
            metadataTag = nbt.load(buf=self.selectedRevision.readFile("level.dat"))
            self.metadata = AnvilWorldMetadata(metadataTag)
            self.loadFMLMapping()
        except (EnvironmentError, zlib.error) as e:
            log.info("Error loading level.dat, trying level.dat_old ({0})".format(e))
            try:
                metadataTag = nbt.load(buf=self.selectedRevision.readFile("level.dat_old"))
                self.metadata = AnvilWorldMetadata(metadataTag)
                log.info("level.dat restored from backup.")
                self.saveChanges()
            except Exception as e:
                traceback.print_exc()
                log.info("%r while loading level.dat_old. Initializing with defaults.", e)
                self._createMetadataTag()

        assert self.metadata.version == VERSION_ANVIL, "Pre-Anvil world formats are not supported (for now)"

    def loadFMLMapping(self):
        metadataTag = self.metadata.metadataTag
        fml = metadataTag.get('FML')
        if fml is None:
            return

        mid = fml.get('ModItemData')  # MC 1.6
        if mid is not None:
            log.info("Adding block IDs from FML for MC 1.6")
            blocktypes = PCBlockTypeSet()
            for entry in mid:
                ID = entry['ItemId'].value
                meta = entry['ordinal'].value
                name = entry['ItemType'].value
                if (ID, 0) not in blocktypes.statesByID:
                    blocktypes.IDsByState[name] = ID, meta
                    blocktypes.statesByID[ID, meta] = name
                    blocktypes.blockJsons[name] = {
                        'displayName':name,
                        'internalName':name,
                        'blockState':'',
                    }
                    blocktypes.allBlocks.append(BlockType(ID, meta, blocktypes))

            blocktypes.allBlocks.sort()
            self.blocktypes = blocktypes

        itemdata = fml.get('ItemData')  # MC 1.7
        if itemdata is not None:
            count = 0
            log.info("Adding block IDs from FML for MC 1.7")
            blocktypes = PCBlockTypeSet()
            for entry in itemdata:
                ID = entry['V'].value
                name = entry['K'].value
                if (ID, 0) not in blocktypes.statesByID:
                    count += 1
                    blocktypes.IDsByState[name] = ID, 0
                    blocktypes.statesByID[ID, 0] = name
                    blocktypes.blockJsons[name] = {
                        'displayName':name,
                        'internalName':name,
                        'blockState':'',
                    }
                    blocktypes.allBlocks.append(BlockType(ID, 0, blocktypes))

            blocktypes.allBlocks.sort()
            log.info("Added %d blocks.", count)
            self.blocktypes = blocktypes


    def _createMetadataTag(self, random_seed=None):
        """
        Create a level.dat for a newly created world or a world found with damaged level.dat/.dat_old (xxx repair in
        WorldEditor?)
        :param random_seed:
        :type random_seed:
        :return:
        :rtype:
        """
        metadataTag = nbt.TAG_Compound()
        metadataTag["Data"] = nbt.TAG_Compound()
        metadataTag["Data"]["SpawnX"] = nbt.TAG_Int(0)
        metadataTag["Data"]["SpawnY"] = nbt.TAG_Int(2)
        metadataTag["Data"]["SpawnZ"] = nbt.TAG_Int(0)

        last_played = long(time.time() * 1000)
        if random_seed is None:
            random_seed = long(random.random() * 0xffffffffffffffffL) - 0x8000000000000000L

        metadataTag["Data"]['version'] = nbt.TAG_Int(VERSION_ANVIL)

        self.metadata = AnvilWorldMetadata(metadataTag)

        self.metadata.LastPlayed = long(last_played)
        self.metadata.RandomSeed = long(random_seed)
        self.metadata.SizeOnDisk = 0
        self.metadata.Time = 1
        self.metadata.LevelName = os.path.basename(self.filename)

    def syncToDisk(self):
        """
        Write cached items (metadata from level.dat and players in players/ folder) to the current revision.
        :return:
        :rtype:
        """
        if self.metadata.dirty:
            self.selectedRevision.writeFile("level.dat", self.metadata.metadataTag.save())
            self.metadata.dirty = False

    def saveChanges(self):
        """
        Write all changes from all revisions into the world folder.

        :return:
        :rtype: None
        """
        if self.readonly:
            raise IOError("World is opened read only.")

        self.checkSessionLock()
        self.revisionHistory.writeAllChanges(self.selectedRevision)
        self.selectedRevision = self.revisionHistory.getHead()

    def close(self):
        """
        Close the world, deleting temporary files and freeing resources. Operations on a closed world are undefined.

        :return:
        :rtype: None
        """
        self.revisionHistory.close()
        pass  # do what here???

    # --- Undo revisions ---

    def requireRevisions(self):
        """
        Enforce the creation of new revisions by making the world folder's revision read-only.
        :return:
        :rtype:
        """
        self.revisionHistory.rootNode.readOnly = True

    def createRevision(self):
        """
        Create a new undo revision. Subsequent changes should be stored in the new revision.

        :return:
        :rtype:
        """
        self.selectedRevision = self.revisionHistory.createRevision(self.selectedRevision)

    def closeRevision(self):
        """
        Close the current revision and mark it read-only. Subsequent edits will not be possible until createRevision
        is called.

        :return:
        :rtype:
        """
        self.revisionHistory.closeRevision()

    def setRevisionInfo(self, info):
        """
        Attach some arbitrary JSON-serializable data to the current revision

        :param info: JSON-serializable data
        :type info: list | dict | str | unicode | int
        """
        self.selectedRevision.setRevisionInfo(info)

    def getRevisionInfo(self):
        """
        Return JSON-serializable data attached previously to the current revision via setRevisionInfo, or None if
        no data is attached.

        :return:
        :rtype: list | dict | str | unicode | int | None
        """
        return self.selectedRevision.getRevisionInfo()

    def selectRevision(self, index):
        """
        Select the current revision by index. Return changes between the previous revision and this one. If
         the index is invalid, returns None and does nothing.

         (XXX use an ID instead of index?)

        :param index:
        :type index:
        :return:
        :rtype: RevisionChanges
        """
        if index < 0 or index >= len(self.revisionHistory.nodes):
            return None
        newRevision = self.revisionHistory.getRevision(index)
        changes = self.revisionHistory.getRevisionChanges(self.selectedRevision, newRevision)
        self.selectedRevision = newRevision
        self.loadMetadata()
        return changes

    def listRevisions(self):
        """
        List the revision indexes and infos as (index, info) tuples. Info is JSON-serializable data previously attached
        with setRevisionInfo, or None.

        :return:
        :rtype: iterator[(int, list | dict | str | unicode | int)]
        """
        for ID, node in enumerate(self.revisionHistory.nodes):
            yield ID, node.getRevisionInfo()

    # --- Session lock ---

    def acquireSessionLock(self):
        """
        Acquire the world's session.lock. Formats without this file may do nothing.

        :return:
        :rtype:
        """
        lockfile = self.revisionHistory.rootFolder.getFilePath("session.lock")
        self.lockTime = int(time.time() * 1000)
        with file(lockfile, "wb") as f:
            f.write(struct.pack(">q", self.lockTime))
            f.flush()
            os.fsync(f.fileno())

    def checkSessionLock(self):
        """
        Make sure the lock previously acquired by acquireSessionLock is still valid. Raise SessionLockLost if it is
        not. Raising the exception will abort any writes done to the main world folder.

        :return:
        :rtype:
        """
        if self.readonly:
            raise SessionLockLost("World is opened read only.")

        lockfile = self.revisionHistory.rootFolder.getFilePath("session.lock")
        try:
            (lock, ) = struct.unpack(">q", file(lockfile, "rb").read())
        except struct.error:
            lock = -1
        if lock != self.lockTime:
            raise SessionLockLost("Session lock lost. This world is being accessed from another location.")

    # --- Format detection ---

    @classmethod
    def canOpenFile(cls, filename):
        """
        Ask this adapter if it can open the given file.

        :param filename: File to identify
        :type filename: str | unicode
        :return:
        :rtype: boolean
        """
        if os.path.exists(os.path.join(filename, "chunks.dat")):
            return False  # exclude Pocket Edition folders

        if not os.path.isdir(filename):
            f = os.path.basename(filename)
            if f not in ("level.dat", "level.dat_old"):
                return False
            filename = os.path.dirname(filename)

        files = os.listdir(filename)
        if "level.dat" in files or "level.dat_old" in files:
            return True

        return False

    # --- Dimensions ---

    def listDimensions(self):
        """
        List the names of all dimensions in this world.

        :return:
        :rtype: iterator of str
        """
        return self.selectedRevision.listDimensions()

    # --- Chunks ---

    def chunkCount(self, dimName):
        """
        Count the chunks in the given dimension

        :param dimName:
        :type dimName: str
        :return:
        :rtype: int
        """
        return self.selectedRevision.chunkCount(dimName)

    def chunkPositions(self, dimName):
        """
        List the chunk positions (cx, cz) in the given dimension.

        :type dimName: unicode or str
        :return:
        :rtype: Iterator of (int, int)
        """
        return iter(self.selectedRevision.chunkPositions(dimName))

    def containsChunk(self, cx, cz, dimName):
        """
        Return whether the given chunk is present in the given dimension

        :type cx: int or dtype
        :type cz: int or dtype
        :type dimName: str
        :return:
        :rtype: bool
        """
        return self.selectedRevision.containsChunk(cx, cz, dimName)

    def readChunk(self, cx, cz, dimName):
        """
        Return chunk (cx, cz) in the given dimension as an AnvilChunkData. Raise ChunkNotPresent if not found.

        :type cx: int or dtype
        :type cz: int or dtype
        :type dimName: str
        :return:
        :rtype: AnvilChunkData
        """
        try:
            data = self.selectedRevision.readChunkBytes(cx, cz, dimName)
            chunkTag = nbt.load(buf=data)
            log.debug("_getChunkData: Chunk %s loaded (%s bytes)", (cx, cz), len(data))
            chunkData = AnvilChunkData(self, cx, cz, dimName, chunkTag)
        except ChunkNotPresent:
            raise
        except (KeyError, IndexError, zlib.error) as e:  # Missing nbt keys, lists too short, decompression failure
            raise AnvilChunkFormatError("Error loading chunk: %r" % e)

        return chunkData

    def writeChunk(self, chunk):
        """
        Write the given AnvilChunkData to the current revision.

        :type chunk: mceditlib.anvil.adapter.AnvilChunkData
        """
        tag = chunk.buildNBTTag()
        self.selectedRevision.writeChunkBytes(chunk.cx, chunk.cz, chunk.dimName, tag.save(compressed=False))

    def createChunk(self, cx, cz, dimName):
        """
        Create a new empty chunk at the given position in the given dimension.

        :type cx: int
        :type cz: int
        :type dimName: str
        :return:
        :rtype: AnvilChunkData
        """
        if self.selectedRevision.containsChunk(cx, cz, dimName):
            raise ValueError("Chunk %s already exists in dim %s", (cx, cz), dimName)
        chunk = AnvilChunkData(self, cx, cz, dimName, create=True)
        self.selectedRevision.writeChunkBytes(cx, cz, dimName, chunk.buildNBTTag().save(compressed=False))
        return chunk

    def deleteChunk(self, cx, cz, dimName):
        """
        Delete the chunk at the given position in the given dimension.

        :type cx: int
        :type cz: int
        :type dimName: str
        """
        self.selectedRevision.deleteChunk(cx, cz, dimName)

    # --- Players ---

    def listPlayers(self):
        """
        List the names of all players in this world (XXX players folder in dimension folders??)

        :return:
        :rtype: Iterator of [str]
        """
        for f in self.selectedRevision.listFolder("playerdata"):
            if f.endswith(".dat"):
                yield f[11:-4]

        if "Player" in self.metadata.rootTag:
            yield ""

    def getPlayer(self, playerUUID=""):
        return AnvilPlayerRef(self, playerUUID)

    def getPlayerTag(self, playerUUID=""):
        """
        Return the root NBT tag for the named player. Raise PlayerNotFound if not present.

        :param playerUUID:
        :type playerUUID: unicode
        :return:
        :rtype: PCPlayer
        """
        if playerUUID == "":
            if "Player" in self.metadata.rootTag:
                # single-player world
                playerTag = self.metadata.rootTag["Player"]
                return playerTag
            raise PlayerNotFound(playerUUID)
        else:
            playerFilePath = "playerdata/%s.dat" % playerUUID
            if self.selectedRevision.containsFile(playerFilePath):
                # multiplayer world, found this player
                playerTag = nbt.load(buf=self.selectedRevision.readFile(playerFilePath))
                return playerTag
            else:
                raise PlayerNotFound(playerUUID)

    def savePlayerTag(self, tag, playerUUID):
        if playerUUID == "":
            # sync metadata?
            self.metadata.dirty = True
        else:
            self.selectedRevision.writeFile("playerdata/%s.dat" % playerUUID, tag.save())

    def createPlayer(self, playerUUID=""):
        """
        Create a new player with the given name and return the PlayerRef. Raises some kind of IOError if the player
         could not be created.

        :param playerUUID:
        :type playerUUID: str
        :return:
        :rtype: PCPlayer
        """
        if self.readonly:
            raise IOError("World is opened read only.")

        playerFilePath = "playerdata/%s.dat" % playerUUID

        if playerUUID == "":
            if "Player" in self.metadata.rootTag["Data"]:
                raise IOError("Single-player player already exists.")
            playerTag = nbt.TAG_Compound()
            self.metadata.rootTag["Data"]["Player"] = playerTag
        else:
            if self.selectedRevision.containsFile(playerFilePath):
                raise ValueError("Cannot create player %s: already exists.")

            playerTag = nbt.TAG_Compound()

        player = AnvilPlayerRef(playerTag, self)
        nbtattr.SetNBTDefaults(player)

        if playerUUID != "Player":
            self.checkSessionLock()
            self.selectedRevision.writeFile(playerFilePath, playerTag.save())

        return self.getPlayer(playerUUID)
Example #2
0
class AnvilWorldAdapter(object):
    """
    Provides an interface to AnvilWorldFolder/RevisionHistory that is usable by WorldEditor

    This interface is the base used for all adapter classes. When writing a new adapter, make sure to
    implement all required methods and attributes. Required methods and attrs are the ones with docstrings.
    """

    minHeight = 0
    maxHeight = 256
    hasLights = True

    def __init__(self, filename=None, create=False, readonly=False, resume=None):
        """
        Load a Minecraft for PC level (Anvil format) from the given filename. It can point to either
        a level.dat or a folder containing one. If create is True, it will
        also create the world using a randomly selected seed.

        If you try to create an existing world, IOError will be raised.

        Uses a RevisionHistory to manage undo history. Upon creation, the world is read-only until createRevision() is
        called. Call createRevision() to create a new revision, or selectRevision() to revert to an earlier
        revision. Older revisions are read-only, so createRevision() must be called again to make further changes.

        Call writeAllChanges() to write all changes into the original world.

        :type filename: str or unicode
        :type create: bool
        :type readonly: bool
        :rtype: AnvilWorldAdapter
        """
        self.lockTime = 0

        self.EntityRef = PCEntityRef
        self.TileEntityRef = PCTileEntityRef

        assert not (create and readonly)

        if os.path.basename(filename) in ("level.dat", "level.dat_old"):
            filename = os.path.dirname(filename)

        if not os.path.exists(filename):
            if not create:
                raise IOError('File not found')

            os.mkdir(filename)
        else:
            if create:
                if not os.path.isdir(filename) or os.path.exists(os.path.join(filename, "level.dat")):
                    raise IOError('File exists!')

        if not os.path.isdir(filename):
            raise IOError('File is not a Minecraft Anvil world')

        if readonly:
            self.revisionHistory = AnvilWorldFolder(filename)
            self.selectedRevision = self.revisionHistory
        else:
            self.revisionHistory = RevisionHistory(filename, resume)
            self.selectedRevision = self.revisionHistory.getHead()

        self.filename = filename
        self.readonly = readonly
        if not readonly:
            self.acquireSessionLock()

        if create:
            self._createMetadataTag()
            self.selectedRevision.writeFile("level.dat", self.metadata.metadataTag.save())

        else:
            self.loadMetadata()



    def __repr__(self):
        return "AnvilWorldAdapter(%r)" % self.filename

    # --- Create, save, close ---

    def loadMetadata(self):
        try:
            metadataTag = nbt.load(buf=self.selectedRevision.readFile("level.dat"))
            self.metadata = AnvilWorldMetadata(metadataTag)
            self.loadBlockMapping()
        except (EnvironmentError, zlib.error, NBTFormatError) as e:
            log.info("Error loading level.dat, trying level.dat_old ({0})".format(e))
            try:
                metadataTag = nbt.load(buf=self.selectedRevision.readFile("level.dat_old"))
                self.metadata = AnvilWorldMetadata(metadataTag)
                self.metadata.dirty = True
                log.info("level.dat restored from backup.")
            except Exception as e:
                traceback.print_exc()
                log.info("%r while loading level.dat_old. Initializing with defaults.", e)
                self._createMetadataTag()

        assert self.metadata.version == VERSION_ANVIL, "Pre-Anvil world formats are not supported (for now)"

    def loadBlockMapping(self):
        if self.metadata.is1_8World():
            itemStackVersion = VERSION_1_8
        else:
            itemStackVersion = VERSION_1_7

        blocktypes = PCBlockTypeSet(itemStackVersion)
        self.blocktypes = blocktypes

        metadataTag = self.metadata.metadataTag
        fml = metadataTag.get('FML')
        if fml is None:
            return

        itemTypes = blocktypes.itemTypes

        itemdata = fml.get('ItemData')  # MC 1.7
        if itemdata is not None:
            count = 0
            log.info("Adding block IDs from FML for MC 1.7")
            replacedIDs = []
            for entry in itemdata:
                ID = entry['V'].value
                name = entry['K'].value
                magic, name = name[0], name[1:]
                if magic == u'\x01':  # 0x01 = blocks

                    if not name.startswith("minecraft:"):
                        # we load 1.8 block IDs and mappings by default
                        # FML IDs should be allowed to override some of them for 1.8 blocks not in 1.7.
                        count += 1
                        replacedIDs.append(ID)
                        fakeState = '[0]'
                        nameAndState = name + fakeState
                        log.debug("FML1.7: Adding %s = %d", name, ID)


                        for vanillaMeta in range(15):
                            # Remove existing Vanilla defs
                            vanillaNameAndState = blocktypes.statesByID.get((ID, vanillaMeta))
                            blocktypes.blockJsons.pop(vanillaNameAndState, None)


                        blocktypes.IDsByState[nameAndState] = ID, 0
                        blocktypes.statesByID[ID, 0] = nameAndState
                        blocktypes.IDsByName[name] = ID
                        blocktypes.namesByID[ID] = name
                        blocktypes.defaultBlockstates[name] = fakeState

                        blocktypes.blockJsons[nameAndState] = {
                            'displayName': name,
                            'internalName': name,
                            'blockState': '[0]',
                            'unknown': True,
                        }

                if magic == u'\x02':  # 0x02 = items
                    if not name.startswith("minecraft:"):
                        itemTypes.addFMLIDMapping(name, ID)

            replacedIDsSet = set(replacedIDs)
            blocktypes.allBlocks[:] = [b for b in blocktypes if b.ID not in replacedIDsSet]
            blocktypes.allBlocks.extend(BlockType(newID, 0, blocktypes) for newID in replacedIDs)

            blocktypes.allBlocks.sort()
            log.info("Added %d blocks.", count)



    def _createMetadataTag(self, random_seed=None):
        """
        Create a level.dat for a newly created world or a world found with damaged level.dat/.dat_old (xxx repair in
        WorldEditor?)
        :param random_seed:
        :type random_seed:
        :return:
        :rtype:
        """
        metadataTag = nbt.TAG_Compound()
        metadataTag["Data"] = nbt.TAG_Compound()
        metadataTag["Data"]["SpawnX"] = nbt.TAG_Int(0)
        metadataTag["Data"]["SpawnY"] = nbt.TAG_Int(2)
        metadataTag["Data"]["SpawnZ"] = nbt.TAG_Int(0)

        last_played = long(time.time() * 1000)
        if random_seed is None:
            random_seed = long(random.random() * 0xffffffffffffffffL) - 0x8000000000000000L

        metadataTag["Data"]['version'] = nbt.TAG_Int(VERSION_ANVIL)

        self.metadata = AnvilWorldMetadata(metadataTag)

        self.metadata.LastPlayed = long(last_played)
        self.metadata.RandomSeed = long(random_seed)
        self.metadata.SizeOnDisk = 0
        self.metadata.Time = 1
        self.metadata.LevelName = os.path.basename(self.filename)

    def syncToDisk(self):
        """
        Write cached items (metadata from level.dat and players in players/ folder) to the current revision.
        :return:
        :rtype:
        """
        if self.metadata.dirty:
            self.selectedRevision.writeFile("level.dat", self.metadata.metadataTag.save())
            self.metadata.dirty = False

    def saveChanges(self):
        """
        Write all changes from all revisions into the world folder.

        :return:
        :rtype: None
        """
        if self.readonly:
            raise IOError("World is opened read only.")

        self.checkSessionLock()
        self.revisionHistory.writeAllChanges(self.selectedRevision)
        self.selectedRevision = self.revisionHistory.getHead()

    def close(self):
        """
        Close the world, deleting temporary files and freeing resources. Operations on a closed world are undefined.

        :return:
        :rtype: None
        """
        self.revisionHistory.close()
        pass  # do what here???

    # --- Undo revisions ---

    def requireRevisions(self):
        """
        Enforce the creation of new revisions by making the world folder's revision read-only.
        :return:
        :rtype:
        """
        self.revisionHistory.rootNode.readOnly = True

    def createRevision(self):
        """
        Create a new undo revision. Subsequent changes should be stored in the new revision.

        :return:
        :rtype:
        """
        self.selectedRevision = self.revisionHistory.createRevision(self.selectedRevision)

    def closeRevision(self):
        """
        Close the current revision and mark it read-only. Subsequent edits will not be possible until createRevision
        is called.

        :return:
        :rtype:
        """
        self.revisionHistory.closeRevision()

    def setRevisionInfo(self, info):
        """
        Attach some arbitrary JSON-serializable data to the current revision

        :param info: JSON-serializable data
        :type info: list | dict | str | unicode | int
        """
        self.selectedRevision.setRevisionInfo(info)

    def getRevisionInfo(self):
        """
        Return JSON-serializable data attached previously to the current revision via setRevisionInfo, or None if
        no data is attached.

        :return:
        :rtype: list | dict | str | unicode | int | None
        """
        return self.selectedRevision.getRevisionInfo()

    def selectRevision(self, index):
        """
        Select the current revision by index. Return changes between the previous revision and this one. If
         the index is invalid, returns None and does nothing.

         (XXX use an ID instead of index?)

        :param index:
        :type index:
        :return:
        :rtype: RevisionChanges
        """
        if index < 0 or index >= len(self.revisionHistory.nodes):
            return None
        newRevision = self.revisionHistory.getRevision(index)
        changes = self.revisionHistory.getRevisionChanges(self.selectedRevision, newRevision)
        self.selectedRevision = newRevision
        self.loadMetadata()
        return changes

    def listRevisions(self):
        """
        List the revision indexes and infos as (index, info) tuples. Info is JSON-serializable data previously attached
        with setRevisionInfo, or None.

        :return:
        :rtype: iterator[(int, list | dict | str | unicode | int)]
        """
        for ID, node in enumerate(self.revisionHistory.nodes):
            yield ID, node.getRevisionInfo()

    # --- Session lock ---

    def acquireSessionLock(self):
        """
        Acquire the world's session.lock. Formats without this file may do nothing.

        :return:
        :rtype:
        """
        lockfile = self.revisionHistory.rootFolder.getFilePath("session.lock")
        self.lockTime = int(time.time() * 1000)
        with file(lockfile, "wb") as f:
            f.write(struct.pack(">q", self.lockTime))
            f.flush()
            os.fsync(f.fileno())

    def checkSessionLock(self):
        """
        Make sure the lock previously acquired by acquireSessionLock is still valid. Raise SessionLockLost if it is
        not. Raising the exception will abort any writes done to the main world folder.

        :return:
        :rtype:
        """
        if self.readonly:
            raise SessionLockLost("World is opened read only.")

        lockfile = self.revisionHistory.rootFolder.getFilePath("session.lock")
        try:
            (lock, ) = struct.unpack(">q", file(lockfile, "rb").read())
        except struct.error:
            lock = -1
        if lock != self.lockTime:
            raise SessionLockLost("Session lock lost. This world is being accessed from another location.")

    # --- Format detection ---

    @classmethod
    def canOpenFile(cls, filename):
        """
        Ask this adapter if it can open the given file.

        :param filename: File to identify
        :type filename: str | unicode
        :return:
        :rtype: boolean
        """
        if os.path.exists(os.path.join(filename, "chunks.dat")):
            return False  # exclude Pocket Edition folders

        if not os.path.isdir(filename):
            f = os.path.basename(filename)
            if f not in ("level.dat", "level.dat_old"):
                return False
            filename = os.path.dirname(filename)

        files = os.listdir(filename)
        if "level.dat" in files or "level.dat_old" in files:
            return True

        return False

    # --- Dimensions ---

    def listDimensions(self):
        """
        List the names of all dimensions in this world.

        :return:
        :rtype: iterator of str
        """
        return self.selectedRevision.listDimensions()

    # --- Chunks ---

    def chunkCount(self, dimName):
        """
        Count the chunks in the given dimension

        :param dimName:
        :type dimName: str
        :return:
        :rtype: int
        """
        return self.selectedRevision.chunkCount(dimName)

    def chunkPositions(self, dimName):
        """
        List the chunk positions (cx, cz) in the given dimension.

        :type dimName: unicode or str
        :return:
        :rtype: Iterator of (int, int)
        """
        return iter(self.selectedRevision.chunkPositions(dimName))

    def containsChunk(self, cx, cz, dimName):
        """
        Return whether the given chunk is present in the given dimension

        :type cx: int or dtype
        :type cz: int or dtype
        :type dimName: str
        :return:
        :rtype: bool
        """
        return self.selectedRevision.containsChunk(cx, cz, dimName)

    def readChunk(self, cx, cz, dimName):
        """
        Return chunk (cx, cz) in the given dimension as an AnvilChunkData. Raise ChunkNotPresent if not found.

        :type cx: int or dtype
        :type cz: int or dtype
        :type dimName: str
        :return:
        :rtype: AnvilChunkData
        """
        try:
            data = self.selectedRevision.readChunkBytes(cx, cz, dimName)
            chunkTag = nbt.load(buf=data)
            log.debug("_getChunkData: Chunk %s loaded (%s bytes)", (cx, cz), len(data))
            chunkData = AnvilChunkData(self, cx, cz, dimName, chunkTag)
        except ChunkNotPresent:
            raise
        except (KeyError, IndexError, zlib.error) as e:  # Missing nbt keys, lists too short, decompression failure
            raise AnvilChunkFormatError("Error loading chunk: %r" % e)

        return chunkData

    def writeChunk(self, chunk):
        """
        Write the given AnvilChunkData to the current revision.

        :type chunk: mceditlib.anvil.adapter.AnvilChunkData
        """
        tag = chunk.buildNBTTag()
        self.selectedRevision.writeChunkBytes(chunk.cx, chunk.cz, chunk.dimName, tag.save(compressed=False))

    def createChunk(self, cx, cz, dimName):
        """
        Create a new empty chunk at the given position in the given dimension.

        :type cx: int
        :type cz: int
        :type dimName: str
        :return:
        :rtype: AnvilChunkData
        """
        if self.selectedRevision.containsChunk(cx, cz, dimName):
            raise ValueError("Chunk %s already exists in dim %s", (cx, cz), dimName)
        chunk = AnvilChunkData(self, cx, cz, dimName, create=True)
        self.selectedRevision.writeChunkBytes(cx, cz, dimName, chunk.buildNBTTag().save(compressed=False))
        return chunk

    def deleteChunk(self, cx, cz, dimName):
        """
        Delete the chunk at the given position in the given dimension.

        :type cx: int
        :type cz: int
        :type dimName: str
        """
        self.selectedRevision.deleteChunk(cx, cz, dimName)

    # --- Players ---

    def listPlayers(self):
        """
        List the names of all players in this world (XXX players folder in dimension folders??)

        :return:
        :rtype: Iterator of [str]
        """
        for f in self.selectedRevision.listFolder("playerdata"):
            if f.endswith(".dat"):
                yield f[11:-4]

        if "Player" in self.metadata.rootTag:
            yield ""

    def getPlayer(self, playerUUID=""):
        return AnvilPlayerRef(self, playerUUID)

    def getPlayerTag(self, playerUUID=""):
        """
        Return the root NBT tag for the named player. Raise PlayerNotFound if not present.

        :param playerUUID:
        :type playerUUID: unicode
        :return:
        :rtype: PCPlayer
        """
        if playerUUID == "":
            if "Player" in self.metadata.rootTag:
                # single-player world
                playerTag = self.metadata.rootTag["Player"]
                return playerTag
            raise PlayerNotFound(playerUUID)
        else:
            playerFilePath = "playerdata/%s.dat" % playerUUID
            if self.selectedRevision.containsFile(playerFilePath):
                # multiplayer world, found this player
                playerTag = nbt.load(buf=self.selectedRevision.readFile(playerFilePath))
                return playerTag
            else:
                raise PlayerNotFound(playerUUID)

    def savePlayerTag(self, tag, playerUUID):
        if playerUUID == "":
            # sync metadata?
            self.metadata.dirty = True
        else:
            self.selectedRevision.writeFile("playerdata/%s.dat" % playerUUID, tag.save())

    def createPlayer(self, playerUUID=""):
        """
        Create a new player with the given name and return the PlayerRef. Raises some kind of IOError if the player
         could not be created.

        :param playerUUID:
        :type playerUUID: str
        :return:
        :rtype: PCPlayer
        """
        if self.readonly:
            raise IOError("World is opened read only.")

        playerFilePath = "playerdata/%s.dat" % playerUUID

        if playerUUID == "":
            if "Player" in self.metadata.rootTag["Data"]:
                raise IOError("Single-player player already exists.")
            playerTag = nbt.TAG_Compound()
            self.metadata.rootTag["Data"]["Player"] = playerTag
        else:
            if self.selectedRevision.containsFile(playerFilePath):
                raise ValueError("Cannot create player %s: already exists.")

            playerTag = nbt.TAG_Compound()

        player = AnvilPlayerRef(playerTag, self)
        nbtattr.SetNBTDefaults(player)

        if playerUUID != "Player":
            self.checkSessionLock()
            self.selectedRevision.writeFile(playerFilePath, playerTag.save())

        return self.getPlayer(playerUUID)
Example #3
0
class AnvilWorldAdapter(object):
    """
    Provides an interface to AnvilWorldFolder/RevisionHistory that is usable by WorldEditor

    This interface is the base used for all adapter classes. When writing a new adapter, make sure to
    implement all required methods and attributes. Required methods and attrs are the ones with docstrings.
    """

    minHeight = 0
    maxHeight = 256
    hasLights = True

    def __init__(self,
                 filename=None,
                 create=False,
                 readonly=False,
                 resume=None):
        """
        Load a Minecraft for PC level (Anvil format) from the given filename. It can point to either
        a level.dat or a folder containing one. If create is True, it will
        also create the world using a randomly selected seed.

        If you try to create an existing world, IOError will be raised.

        Uses a RevisionHistory to manage undo history. Upon creation, the world is read-only until createRevision() is
        called. Call createRevision() to create a new revision, or selectRevision() to revert to an earlier
        revision. Older revisions are read-only, so createRevision() must be called again to make further changes.

        Call writeAllChanges() to write all changes into the original world.

        :type filename: str or unicode
        :type create: bool
        :type readonly: bool
        :rtype: AnvilWorldAdapter
        """
        self.lockTime = 0

        self.EntityRef = PCEntityRef
        self.TileEntityRef = PCTileEntityRef

        assert not (create and readonly)

        if os.path.basename(filename) in ("level.dat", "level.dat_old"):
            filename = os.path.dirname(filename)

        if not os.path.exists(filename):
            if not create:
                raise IOError('File not found')

            os.mkdir(filename)
        else:
            if create:
                if not os.path.isdir(filename) or os.path.exists(
                        os.path.join(filename, "level.dat")):
                    raise IOError('File exists!')

        if not os.path.isdir(filename):
            raise IOError('File is not a Minecraft Anvil world')

        if readonly:
            self.revisionHistory = AnvilWorldFolder(filename, readonly=True)
            self.selectedRevision = self.revisionHistory
        else:
            self.revisionHistory = RevisionHistory(filename, resume)
            self.selectedRevision = self.revisionHistory.getHead()

        self.filename = filename
        self.readonly = readonly
        if not readonly:
            self.acquireSessionLock()

        if create:
            self._createMetadataTag()
            self.selectedRevision.writeFile("level.dat",
                                            self.metadata.metadataTag.save())

        else:
            self.loadMetadata()

        self.loadBlockMapping()

    def __repr__(self):
        return "AnvilWorldAdapter(%r)" % self.filename

    # --- Summary info ---

    @classmethod
    def getWorldInfo(cls, filename, displayNameLimit=40):
        try:
            if os.path.isdir(filename):
                folderName = os.path.basename(filename)
                levelDat = os.path.join(filename, "level.dat")
            else:
                folderName = os.path.basename(os.path.dirname(filename))
                levelDat = filename

            levelTag = nbt.load(levelDat)
            try:
                displayName = levelTag['Data']['LevelName'].value

                if len(displayName) > displayNameLimit:
                    displayName = displayName[:displayNameLimit] + "..."
                if len(folderName) > displayNameLimit:
                    folderName = folderName[:displayNameLimit] + "..."

                if folderName != displayName:
                    displayName = "%s (%s)" % (displayName, folderName)
            except Exception as e:
                log.warn("Failed to get display name for level.", exc_info=1)
                displayName = folderName

            try:
                lastPlayedTime = levelTag['Data']['LastPlayed'].value
            except Exception as e:
                log.warn("Failed to get last-played time for level.",
                         exc_info=1)
                lastPlayedTime = 0

            version = "Unknown Version"
            try:
                try:
                    metadata = AnvilWorldMetadata(levelTag)
                    versionTag = metadata.Version
                    if versionTag.Snapshot:
                        version = "Minecraft Snapshot " + versionTag.Name
                    else:
                        version = "Minecraft " + versionTag.Name

                except Exception as e:

                    stackVersion = VERSION_1_8 if metadata.is1_8World(
                    ) else VERSION_1_7

                    if stackVersion == VERSION_1_7:
                        version = "Minecraft 1.7"
                        if "FML" in metadata.metadataTag:
                            version = "MinecraftForge 1.7"

                    if stackVersion == VERSION_1_8:
                        version = "Minecraft 1.8+"

            except Exception as e:
                log.warn("Failed to get version info for %s: %r",
                         filename,
                         e,
                         exc_info=1)

            return WorldInfo(displayName, lastPlayedTime, version)

        except Exception as e:
            log.error("Failed getting world info for %s: %r", filename, e)
            return WorldInfo(str(e), 0, "")

    # --- Create, save, close ---

    def loadMetadata(self):
        try:
            metadataTag = nbt.load(
                buf=self.selectedRevision.readFile("level.dat"))
            self.metadata = AnvilWorldMetadata(metadataTag)
        except (EnvironmentError, zlib.error, NBTFormatError) as e:
            log.info(
                "Error loading level.dat, trying level.dat_old ({0})".format(
                    e))
            try:
                metadataTag = nbt.load(
                    buf=self.selectedRevision.readFile("level.dat_old"))
                self.metadata = AnvilWorldMetadata(metadataTag)
                self.metadata.dirty = True
                log.info("level.dat restored from backup.")
            except Exception as e:
                traceback.print_exc()
                log.info(
                    "%r while loading level.dat_old. Initializing with defaults.",
                    e)
                self._createMetadataTag()

        if self.metadata.version != VERSION_ANVIL:
            raise LevelFormatError(
                "Pre-Anvil world formats are not supported (for now)")

    def loadBlockMapping(self):
        if self.metadata.is1_8World():
            itemStackVersion = VERSION_1_8
        else:
            itemStackVersion = VERSION_1_7

        blocktypes = PCBlockTypeSet(itemStackVersion)
        self.blocktypes = blocktypes

        metadataTag = self.metadata.metadataTag
        fml = metadataTag.get('FML')
        if fml is None:
            return

        itemTypes = blocktypes.itemTypes

        itemdata = fml.get('ItemData')  # MC 1.7
        if itemdata is not None:
            count = 0
            log.info("Adding block IDs from FML for MC 1.7")
            replacedIDs = []
            for entry in itemdata:
                ID = entry['V'].value
                name = entry['K'].value
                magic, name = name[0], name[1:]
                if magic == u'\x01':  # 0x01 = blocks

                    if not name.startswith("minecraft:"):
                        # we load 1.9 block IDs and mappings by default
                        # FML IDs should be allowed to override some of them for 1.8 blocks not in 1.7.
                        count += 1
                        replacedIDs.append(ID)
                        fakeState = '[meta=0]'
                        nameAndState = name + fakeState
                        log.debug("FML1.7: Adding %s = %d", name, ID)

                        for vanillaMeta in range(15):
                            # Remove existing Vanilla defs
                            vanillaNameAndState = blocktypes.statesByID.get(
                                (ID, vanillaMeta))
                            blocktypes.blockJsons.pop(vanillaNameAndState,
                                                      None)

                            # Also remove Vanilla name<->state mapping
                            blocktypes.IDsByState.pop(vanillaNameAndState,
                                                      None)
                            vanillaName = blocktypes.namesByID.get(ID)
                            blocktypes.IDsByName.pop(vanillaName, None)
                            blocktypes.defaultBlockstates.pop(
                                vanillaName, None)

                        blocktypes.IDsByState[nameAndState] = ID, 0
                        blocktypes.statesByID[ID, 0] = nameAndState
                        blocktypes.IDsByName[name] = ID
                        blocktypes.namesByID[ID] = name
                        blocktypes.defaultBlockstates[name] = fakeState

                        blocktypes.blockJsons[nameAndState] = {
                            'displayName': name,
                            'internalName': name,
                            'blockState': '[meta=0]',
                            'unknown': True,
                        }

                if magic == u'\x02':  # 0x02 = items
                    if not name.startswith("minecraft:"):
                        itemTypes.addFMLIDMapping(name, ID)

            replacedIDsSet = set(replacedIDs)
            blocktypes.discardIDs(replacedIDsSet)
            blocktypes.addBlocktypes(
                BlockType(newID, 0, blocktypes) for newID in replacedIDs)

            log.info("Added %d blocks.", count)

    def _createMetadataTag(self, random_seed=None):
        """
        Create a level.dat for a newly created world or a world found with damaged level.dat/.dat_old (xxx repair in
        WorldEditor?)
        :param random_seed:
        :type random_seed:
        :return:
        :rtype:
        """
        metadataTag = nbt.TAG_Compound()
        metadataTag["Data"] = nbt.TAG_Compound()
        metadataTag["Data"]["SpawnX"] = nbt.TAG_Int(0)
        metadataTag["Data"]["SpawnY"] = nbt.TAG_Int(2)
        metadataTag["Data"]["SpawnZ"] = nbt.TAG_Int(0)

        last_played = long(time.time() * 1000)
        if random_seed is None:
            random_seed = long(
                random.random() * 0xffffffffffffffffL) - 0x8000000000000000L

        metadataTag["Data"]['version'] = nbt.TAG_Int(VERSION_ANVIL)

        self.metadata = AnvilWorldMetadata(metadataTag)

        self.metadata.LastPlayed = long(last_played)
        self.metadata.RandomSeed = long(random_seed)
        self.metadata.SizeOnDisk = 0
        self.metadata.Time = 1
        self.metadata.LevelName = os.path.basename(self.filename)

    def syncToDisk(self):
        """
        Write cached items (metadata from level.dat and players in players/ folder) to the current revision.
        :return:
        :rtype:
        """
        if self.metadata.dirty:
            self.selectedRevision.writeFile("level.dat",
                                            self.metadata.metadataTag.save())
            self.metadata.dirty = False

    def saveChanges(self):
        exhaust(self.saveChangesIter())

    def saveChangesIter(self):
        """
        Write all changes from all revisions into the world folder.

        :return:
        :rtype: None
        """
        if self.readonly:
            raise IOError("World is opened read only.")

        self.checkSessionLock()
        index = self.revisionHistory.nodes.index(self.selectedRevision)
        for status in self.revisionHistory.writeAllChangesIter(
                self.selectedRevision):
            yield status

        self.selectedRevision = self.revisionHistory.nodes[index]
        yield

    def close(self):
        """
        Close the world, deleting temporary files and freeing resources. Operations on a closed world are undefined.

        :return:
        :rtype: None
        """
        self.revisionHistory.close()
        pass  # do what here???

    # --- Undo revisions ---

    def requireRevisions(self):
        """
        Enforce the creation of new revisions by making the world folder's revision read-only.
        :return:
        :rtype:
        """
        self.revisionHistory.rootNode.readOnly = True

    def createRevision(self):
        """
        Create a new undo revision. Subsequent changes should be stored in the new revision.

        :return:
        :rtype:
        """
        self.selectedRevision = self.revisionHistory.createRevision(
            self.selectedRevision)

    def closeRevision(self):
        """
        Close the current revision and mark it read-only. Subsequent edits will not be possible until createRevision
        is called.

        :return:
        :rtype:
        """
        self.revisionHistory.closeRevision()

    def setRevisionInfo(self, info):
        """
        Attach some arbitrary JSON-serializable data to the current revision

        :param info: JSON-serializable data
        :type info: list | dict | str | unicode | int
        """
        self.selectedRevision.setRevisionInfo(info)

    def getRevisionInfo(self):
        """
        Return JSON-serializable data attached previously to the current revision via setRevisionInfo, or None if
        no data is attached.

        :return:
        :rtype: list | dict | str | unicode | int | None
        """
        return self.selectedRevision.getRevisionInfo()

    def selectRevision(self, index):
        """
        Select the current revision by index. Return changes between the previous revision and this one. If
         the index is invalid, returns None and does nothing.

         (XXX use an ID instead of index?)

        :param index:
        :type index:
        :return:
        :rtype: RevisionChanges
        """
        if index < 0 or index >= len(self.revisionHistory.nodes):
            return None
        newRevision = self.revisionHistory.getRevision(index)
        changes = self.revisionHistory.getRevisionChanges(
            self.selectedRevision, newRevision)
        self.selectedRevision = newRevision
        self.loadMetadata()
        return changes

    def listRevisions(self):
        """
        List the revision indexes and infos as (index, info) tuples. Info is JSON-serializable data previously attached
        with setRevisionInfo, or None.

        :return:
        :rtype: iterator[(int, list | dict | str | unicode | int)]
        """
        for ID, node in enumerate(self.revisionHistory.nodes):
            yield ID, node.getRevisionInfo()

    def getRevisionChanges(self, oldRevision, newRevision):
        return self.revisionHistory.getRevisionChanges(oldRevision,
                                                       newRevision)

    # --- Session lock ---

    def acquireSessionLock(self):
        """
        Acquire the world's session.lock. Formats without this file may do nothing.

        :return:
        :rtype:
        """

        lockfile = self.revisionHistory.rootFolder.getFilePath("session.lock")
        self.lockTime = int(time.time() * 1000)
        with open(lockfile, "wb") as f:
            f.write(struct.pack(">q", self.lockTime))
            f.flush()
            os.fsync(f.fileno())

    def checkSessionLock(self):
        """
        Make sure the lock previously acquired by acquireSessionLock is still valid. Raise SessionLockLost if it is
        not. Raising the exception will abort any writes done to the main world folder.

        :return:
        :rtype:
        """
        if self.readonly:
            raise SessionLockLost("World is opened read only.")

        lockfile = self.revisionHistory.rootFolder.getFilePath("session.lock")
        try:
            (lock, ) = struct.unpack(">q", open(lockfile, "rb").read())
        except struct.error:
            lock = -1
        if lock != self.lockTime:
            raise SessionLockLost(
                "Session lock lost. This world is being accessed from another location."
            )

    def stealSessionLock(self):
        if self.readonly:
            raise IOError("World is opened read only.")
        self.acquireSessionLock()

    # --- Format detection ---

    @classmethod
    def canOpenFile(cls, filename):
        """
        Ask this adapter if it can open the given file.

        :param filename: File to identify
        :type filename: str | unicode
        :return:
        :rtype: boolean
        """
        if os.path.exists(os.path.join(filename, "chunks.dat")):
            return False  # exclude Pocket Edition folders

        if not os.path.isdir(filename):
            f = os.path.basename(filename)
            if f not in ("level.dat", "level.dat_old"):
                return False
            filename = os.path.dirname(filename)

        files = os.listdir(filename)
        if "level.dat" in files or "level.dat_old" in files:
            return True

        return False

    # --- Dimensions ---

    def listDimensions(self):
        """
        List the names of all dimensions in this world.

        :return:
        :rtype: iterator of str
        """
        return self.selectedRevision.listDimensions()

    # --- Chunks ---

    def chunkCount(self, dimName):
        """
        Count the chunks in the given dimension

        :param dimName:
        :type dimName: str
        :return:
        :rtype: int
        """
        return self.selectedRevision.chunkCount(dimName)

    def chunkPositions(self, dimName):
        """
        List the chunk positions (cx, cz) in the given dimension.

        :type dimName: unicode or str
        :return:
        :rtype: Iterator of (int, int)
        """
        return iter(self.selectedRevision.chunkPositions(dimName))

    def containsChunk(self, cx, cz, dimName):
        """
        Return whether the given chunk is present in the given dimension

        :type cx: int or dtype
        :type cz: int or dtype
        :type dimName: str
        :return:
        :rtype: bool
        """
        return self.selectedRevision.containsChunk(cx, cz, dimName)

    def readChunk(self, cx, cz, dimName):
        """
        Return chunk (cx, cz) in the given dimension as an AnvilChunkData. Raise ChunkNotPresent if not found.

        :type cx: int or dtype
        :type cz: int or dtype
        :type dimName: str
        :return:
        :rtype: AnvilChunkData
        """
        try:
            data = self.selectedRevision.readChunkBytes(cx, cz, dimName)
            chunkTag = nbt.load(buf=data)
            log.debug("_getChunkData: Chunk %s loaded (%s bytes)", (cx, cz),
                      len(data))
            chunkData = AnvilChunkData(self, cx, cz, dimName, chunkTag)
        except ChunkNotPresent:
            raise
        except (
                KeyError, IndexError, zlib.error, UnicodeError
        ) as e:  # Missing nbt keys, lists too short, decompression failure, unknown NBT tags
            raise AnvilChunkFormatError("Error loading chunk: %r" % e, None,
                                        sys.exc_info()[2])

        return chunkData

    def writeChunk(self, chunk):
        """
        Write the given AnvilChunkData to the current revision.

        :type chunk: mceditlib.anvil.adapter.AnvilChunkData
        """
        tag = chunk.buildNBTTag()
        self.selectedRevision.writeChunkBytes(chunk.cx, chunk.cz,
                                              chunk.dimName,
                                              tag.save(compressed=False))

    def createChunk(self, cx, cz, dimName):
        """
        Create a new empty chunk at the given position in the given dimension.

        :type cx: int
        :type cz: int
        :type dimName: str
        :return:
        :rtype: AnvilChunkData
        """
        if self.selectedRevision.containsChunk(cx, cz, dimName):
            raise ValueError("Chunk %s already exists in dim %r" %
                             ((cx, cz), dimName))
        chunk = AnvilChunkData(self, cx, cz, dimName, create=True)
        self.selectedRevision.writeChunkBytes(
            cx, cz, dimName,
            chunk.buildNBTTag().save(compressed=False))
        return chunk

    def deleteChunk(self, cx, cz, dimName):
        """
        Delete the chunk at the given position in the given dimension.

        :type cx: int
        :type cz: int
        :type dimName: str
        """
        self.selectedRevision.deleteChunk(cx, cz, dimName)

    # --- Players ---

    def listPlayers(self):
        """
        List the names/UUIDs of all players in this world
        (XXX players folder in dimension folders??)

        :return:
        :rtype: Iterator of unicode
        """
        for f in self.selectedRevision.listFolder("playerdata"):
            if f.endswith(".dat"):
                yield f[11:-4]

        if "Player" in self.metadata.rootTag:
            yield ""

    def getPlayer(self, playerUUID=""):
        return AnvilPlayerRef(self, playerUUID)

    def getPlayerTag(self, playerUUID=""):
        """
        Return the root NBT tag for the named player. Raise PlayerNotFound if not present.

        Parameters
        ----------
        playerUUID : unicode
            The player ID returned from :ref:`listPlayers`

        Returns
        -------
        player : AnvilPlayerRef
        """
        if playerUUID == "":
            if "Player" in self.metadata.rootTag:
                # single-player world
                playerTag = self.metadata.rootTag["Player"]
                return playerTag
            raise PlayerNotFound(playerUUID)
        else:
            playerFilePath = "playerdata/%s.dat" % playerUUID
            if self.selectedRevision.containsFile(playerFilePath):
                # multiplayer world, found this player
                playerTag = nbt.load(
                    buf=self.selectedRevision.readFile(playerFilePath))
                return playerTag
            else:
                raise PlayerNotFound(playerUUID)

    def savePlayerTag(self, tag, playerUUID):
        if playerUUID == "":
            # sync metadata?
            self.metadata.dirty = True
        else:
            self.selectedRevision.writeFile("playerdata/%s.dat" % playerUUID,
                                            tag.save())

    def createPlayer(self, playerUUID=""):
        """
        Create a new player with the given name and return the PlayerRef. Raises some kind of IOError if the player
         could not be created.

        :param playerUUID:
        :type playerUUID: str
        :return:
        :rtype: PCPlayer
        """
        if self.readonly:
            raise IOError("World is opened read only.")

        playerFilePath = "playerdata/%s.dat" % playerUUID

        if playerUUID == "":
            if "Player" in self.metadata.rootTag["Data"]:
                raise IOError("Single-player player already exists.")
            playerTag = nbt.TAG_Compound()
            self.metadata.rootTag["Data"]["Player"] = playerTag
        else:
            if self.selectedRevision.containsFile(playerFilePath):
                raise ValueError("Cannot create player %s: already exists.")

            playerTag = nbt.TAG_Compound()

        player = AnvilPlayerRef(playerTag, self)
        nbtattr.SetNBTDefaults(player)

        if playerUUID != "Player":
            self.checkSessionLock()
            self.selectedRevision.writeFile(playerFilePath, playerTag.save())

        return self.getPlayer(playerUUID)

    # --- Maps ---

    def listMaps(self):
        """
        Return a list of map IDs for this world's map items.

        :return: list[object]
        """
        mapRE = re.compile(r'map_(\d+)\.dat')
        for filename in self.selectedRevision.listFolder("data"):
            basename = filename.split("/")[-1]
            match = mapRE.match(basename)
            if match is None or len(match.groups()) == 0:
                continue

            mapID = match.group(1)
            yield int(mapID)

    def getMap(self, mapID):
        return AnvilMapData(self.getMapTag(mapID), mapID, self)

    def _getMapPath(self, mapID):
        return "data/map_%s.dat" % mapID

    def getMapTag(self, mapID):
        mapPath = self._getMapPath(mapID)
        if not self.selectedRevision.containsFile(mapPath):
            raise KeyError("Map %s not found" % mapID)

        mapData = self.selectedRevision.readFile(mapPath)
        mapNBT = nbt.load(buf=mapData)
        return mapNBT

    def saveMapTag(self, mapID, mapTag):
        self.selectedRevision.writeFile(self._getMapPath(mapID), mapTag.save())

    def createMap(self):
        # idcounts.dat should hold the ID number of the last created map
        # but we can't trust it because of bugs in the old map import filters
        mapIDs = list(self.listMaps())
        if len(mapIDs):
            maximumID = max(mapIDs)
            mapID = maximumID + 1
        else:
            mapID = 0

        idcountsTag = nbt.TAG_Compound()
        idcountsTag["map"] = nbt.TAG_Short(mapID)

        # idcounts.dat is not compressed.
        self.selectedRevision.writeFile("data/idcounts.dat",
                                        idcountsTag.save(compressed=False))

        mapData = AnvilMapData.create(mapID, self)

        mapData.save()
        return mapData

    def deleteMap(self, mapID):
        self.selectedRevision.deleteFile(self._getMapPath(mapID))

    # --- Metadata ---

    def getWorldVersionInfo(self):
        versionTag = self.metadata.Version
        return VersionInfo('java', versionTag.Id, versionTag.Name,
                           versionTag.Snapshot)