from __future__ import absolute_import
import collections
import logging
import time
import weakref
import itertools
import numpy
import re
from mceditlib import cachefunc
from mceditlib.block_copy import copyBlocksIter
from mceditlib.blocktypes import BlockType
from mceditlib.nbtattr import NBTListProxy
from mceditlib.operations.block_fill import FillBlocksOperation
from mceditlib.operations.analyze import AnalyzeOperation
from mceditlib.selection import BoundingBox
from mceditlib.findadapter import findAdapter
from mceditlib.multi_block import getBlocks, setBlocks
from mceditlib.schematic import createSchematic
from mceditlib.util import displayName, chunk_pos, exhaust, matchEntityTags
from mceditlib.util.lazyprop import weakrefprop
from mceditlib.blocktypes import BlockType
log = logging.getLogger(__name__)
DIM_NETHER = -1
DIM_END = 1
_zeros = {}
def string_func(array):
numpy.set_string_function(None)
string = repr(array)
string = string[:-1] + ", shape=%s)" % (array.shape,)
numpy.set_string_function(string_func)
return string
numpy.set_string_function(string_func)
class EntityListProxy(collections.MutableSequence):
"""
A proxy for the Entities and TileEntities lists of a WorldEditorChunk. Accessing an element returns an EntityRef
or TileEntityRef wrapping the element of the underlying NBT compound, with a reference to the WorldEditorChunk.
These Refs cannot be created at load time as they hold a reference to the chunk, preventing the chunk from being
unloaded when its refcount reaches zero.
"""
chunk = weakrefprop()
def __init__(self, chunk, attrName, refClass):
self.attrName = attrName
self.refClass = refClass
self.chunk = chunk
def __getitem__(self, key):
return self.refClass(getattr(self.chunk.chunkData, self.attrName)[key], self.chunk)
def __setitem__(self, key, value):
tagList = getattr(self.chunk.chunkData, self.attrName)
if isinstance(key, slice):
tagList[key] = [v.rootTag for v in value]
else:
tagList[key] = value.rootTag
self.chunk.dirty = True
def __delitem__(self, key):
del getattr(self.chunk.chunkData, self.attrName)[key]
self.chunk.dirty = True
def __len__(self):
return len(getattr(self.chunk.chunkData, self.attrName))
def insert(self, index, value):
getattr(self.chunk.chunkData, self.attrName).insert(index, value.rootTag)
self.chunk.dirty = True
def remove(self, value):
getattr(self.chunk.chunkData, self.attrName).remove(value.rootTag)
self.chunk.dirty = True
class WorldEditorChunk(object):
"""
This is a 16x16xH chunk in a format-independent world.
The Blocks, Data, SkyLight, and BlockLight arrays are divided into
vertical sections of 16x16x16, accessed using the `getSection` method.
"""
def __init__(self, chunkData, editor):
self.worldEditor = editor
self.chunkData = chunkData
self.cx, self.cz = chunkData.cx, chunkData.cz
self.dimName = chunkData.dimName
self.dimension = editor.getDimension(self.dimName)
self.Entities = EntityListProxy(self, "Entities", editor.adapter.EntityRef)
self.TileEntities = EntityListProxy(self, "TileEntities", editor.adapter.TileEntityRef)
#self.Entities = [editor.adapter.EntityRef(tag, self) for tag in chunkData.Entities]
#self.TileEntities = [editor.adapter.TileEntityRef(tag, self) for tag in chunkData.TileEntities]
def buildNBTTag(self):
return self.chunkData.buildNBTTag()
def __str__(self):
return u"WorldEditorChunk, coords:{0}, world: {1}, dim: {2} D:{3}".format(
(self.cx, self.cz),
self.worldEditor.displayName,
self.dimName, self.dirty)
# --- WorldEditorChunkData accessors ---
@property
def bounds(self):
return self.chunkData.bounds
@property
def chunkPosition(self):
return self.cx, self.cz
@property
def rootTag(self):
return self.chunkData.rootTag
@property
def dirty(self):
return self.chunkData.dirty
@dirty.setter
def dirty(self, val):
self.chunkData.dirty = val
# --- Chunk attributes ---
def sectionPositions(self):
return self.chunkData.sectionPositions()
def getSection(self, cy, create=False):
return self.chunkData.getSection(cy, create)
@property
def blocktypes(self):
return self.dimension.blocktypes
@property
def Biomes(self):
return self.chunkData.Biomes
@property
def HeightMap(self):
return self.chunkData.HeightMap
@property
def TerrainPopulated(self):
return self.chunkData.TerrainPopulated
@TerrainPopulated.setter
def TerrainPopulated(self, val):
self.chunkData.TerrainPopulated = val
def addEntity(self, ref):
if ref.chunk is self:
return
self.chunkData.Entities.append(ref.rootTag)
ref.chunk = self
self.dirty = True
def removeEntity(self, ref):
self.chunkData.Entities.remove(ref.rootTag)
ref.chunk = None
self.dirty = True
def removeEntities(self, entities):
for ref in entities: # xxx O(n*m)
self.removeEntity(ref)
def addTileEntity(self, ref):
if ref.chunk is self:
return
self.chunkData.TileEntities.append(ref.rootTag)
ref.chunk = self
self.dirty = True
def removeTileEntity(self, ref):
if ref.chunk is not self:
return
self.chunkData.TileEntities.remove(ref.rootTag)
ref.chunk = None
ref.rootTag = None
self.dirty = True
@property
def TileTicks(self):
"""
Directly accesses the TAG_List of TAG_Compounds. Not protected by Refs like Entities and TileEntities are.
:return:
:rtype:
"""
return self.chunkData.TileTicks
class WorldEditor(object):
def __init__(self, filename=None, create=False, readonly=False, adapterClass=None, adapter=None, resume=None):
"""
Load a Minecraft level of any format from the given filename.
If you try to create an existing world, IOError will be raised.
:type filename: str or unknown or unicode
:type create: bool
:type readonly: bool
:type adapter: mceditlib.anvil.adapter.AnvilWorldAdapter or mceditlib.schematic.SchematicFileAdapter
:type adapterClass: class
:type resume: None or bool
:return:
:rtype: WorldEditor
"""
self.playerCache = {}
assert not (create and readonly)
assert not create or adapterClass, "create=True requires an adapterClass"
if adapter:
self.adapter = adapter
elif adapterClass:
self.adapter = adapterClass(filename, create, readonly, resume=resume)
else:
self.adapter = findAdapter(filename, readonly, resume=resume)
self.filename = filename
self.readonly = readonly
# maps (cx, cz, dimName) tuples to WorldEditorChunk
self._loadedChunks = weakref.WeakValueDictionary()
# caches ChunkData from adapter
self._chunkDataCache = cachefunc.lru_cache_object(self._getChunkDataRaw, 1000)
self._chunkDataCache.should_decache = self._shouldUnloadChunkData
self._chunkDataCache.will_decache = self._willUnloadChunkData
# caches recently used WorldEditorChunks
self.recentChunks = collections.deque(maxlen=100)
self._allChunks = None
self.dimensions = {}
self.currentRevision = 0
def __repr__(self):
return "WorldEditor(adapter=%r)" % self.adapter
# --- Summary Info ---
@classmethod
def getWorldInfo(cls, filename):
worldInfo = findAdapter(filename, readonly=True, getInfo=True)
return worldInfo
# --- Forwarded from Adapter ---
@property
def EntityRef(self):
return self.adapter.EntityRef
@property
def TileEntityRef(self):
return self.adapter.TileEntityRef
# --- Debug ---
def setCacheLimit(self, size):
self._chunkDataCache.setCacheLimit(size)
# --- Undo/redo ---
def requireRevisions(self):
self.adapter.requireRevisions()
def undo(self):
self.gotoRevision(self.currentRevision - 1)
def redo(self):
self.gotoRevision(self.currentRevision + 1)
def beginUndo(self):
"""
Begin a new undo revision, creating a new revision in the underlying storage chain if an editable
revision is not selected.
:return:
:rtype:
"""
self.adapter.createRevision()
self.currentRevision += 1
log.info("Opened revision %d", self.currentRevision)
def commitUndo(self, revisionInfo=None):
exhaust(self.commitUndoIter(revisionInfo))
def commitUndoIter(self, revisionInfo=None):
"""
Record all changes since the last call to beginUndo into the adapter's current revision. The revision is closed
and beginUndo must be called to open the next revision.
:param revisionInfo: May be supplied to record metadata for this undo
:type revisionInfo: object | None
:return:
:rtype:
"""
self.adapter.setRevisionInfo(revisionInfo)
for status in self.syncToDiskIter():
yield status
self.adapter.closeRevision()
log.info("Closed revision %d", self.currentRevision)
def undoRevisions(self):
"""
Iterate through all revisions and return (index, revisionInfo) tuples. revisionInfo is the info stored with
commitUndo for each revision. Call selectUndoRevision with the index of the desired revision to rewind time.
:return:
:rtype:
"""
for index, revision in self.adapter.listRevisions():
yield index, revision.revisionInfo()
def gotoRevision(self, index):
"""
:param index:
:type index:
:return:
:rtype:
"""
assert index is not None, "None is not a revision index!"
self.syncToDisk()
self.playerCache.clear()
changes = self.adapter.selectRevision(index)
self.currentRevision = index
if changes is None:
return
log.info("Going to revision %d", index)
log.debug("Changes: %s", changes)
self.recentChunks.clear()
for dimName, chunkPositions in changes.chunks.iteritems():
for cx, cz in chunkPositions:
self._chunkDataCache.decache(cx, cz, dimName)
self._loadedChunks.pop((cx, cz, dimName), None)
# xxx slow, scan changes for chunks and check if they are added/removed
self._allChunks = None
def getRevisionChanges(self, oldIndex, newIndex):
return self.adapter.getRevisionChanges(oldIndex, newIndex)
# --- Save ---
def syncToDisk(self):
exhaust(self.syncToDiskIter())
def syncToDiskIter(self):
"""
Write all loaded chunks, player files, etc to disk.
:return:
:rtype:
"""
dirtyPlayers = 0
for player in self.playerCache.itervalues():
# xxx should be in adapter?
if player.dirty:
dirtyPlayers += 1
player.save()
dirtyChunkCount = 0
for i, (cx, cz, dimName) in enumerate(self._chunkDataCache):
yield i, len(self._chunkDataCache), "Writing modified chunks"
chunkData = self._chunkDataCache(cx, cz, dimName)
if chunkData.dirty:
dirtyChunkCount += 1
self.adapter.writeChunk(chunkData)
chunkData.dirty = False
self.adapter.syncToDisk()
log.info(u"Saved %d chunks and %d players", dirtyChunkCount, dirtyPlayers)
def saveChanges(self):
exhaust(self.saveChangesIter())
def saveChangesIter(self):
if self.readonly:
raise IOError("World is opened read only.")
self.syncToDisk()
self.playerCache.clear()
for status in self.adapter.saveChangesIter():
yield status
def stealSessionLock(self):
if hasattr(self.adapter, "stealSessionLock"):
self.adapter.stealSessionLock()
def saveToFile(self, filename):
# XXXX only works with .schematics!!!
self.adapter.saveToFile(filename)
def close(self):
"""
Unload all chunks and close all open filehandles.
"""
self.adapter.close()
self.recentChunks.clear()
self._allChunks = None
self._loadedChunks.clear()
self._chunkDataCache.clear()
# --- World limits ---
@property
def maxHeight(self):
return self.adapter.maxHeight
# --- World info ---
@property
def displayName(self):
return displayName(self.filename)
@property
def blocktypes(self):
return self.adapter.blocktypes
# --- Chunk I/O ---
def preloadChunkPositions(self):
log.info(u"Scanning for regions in %s...", self.adapter.filename)
self._allChunks = collections.defaultdict(set)
for dimName in self.adapter.listDimensions():
start = time.time()
chunkPositions = set(self.adapter.chunkPositions(dimName))
chunkPositions.update((cx, cz) for cx, cz, cDimName in self._chunkDataCache if cDimName == dimName)
log.info("Dim %s: Found %d chunks in %0.2f seconds.",
dimName,
len(chunkPositions),
time.time() - start)
self._allChunks[dimName] = chunkPositions
def chunkCount(self, dimName):
return self.adapter.chunkCount(dimName)
def chunkPositions(self, dimName):
"""
Iterates over (xPos, zPos) tuples, one for each chunk in the given dimension.
May initiate a costly chunk scan.
:param dimName: Name of dimension
:type dimName: unicode
:return:
:rtype:
"""
if self._allChunks is None:
self.preloadChunkPositions()
return self._allChunks[dimName].__iter__()
def _getChunkDataRaw(self, cx, cz, dimName):
"""
Wrapped by cachefunc.lru_cache in __init__
"""
return self.adapter.readChunk(cx, cz, dimName)
def _shouldUnloadChunkData(self, key):
return key not in self._loadedChunks
def _willUnloadChunkData(self, chunkData):
if chunkData.dirty and not self.readonly:
self.adapter.writeChunk(chunkData)
def getChunk(self, cx, cz, dimName, create=False):
"""
:return: Chunk at the given position.
:rtype: WorldEditorChunk
"""
if create and not self.containsChunk(cx, cz, dimName):
self.createChunk(cx, cz, dimName)
chunk = self._loadedChunks.get((cx, cz, dimName))
if chunk is not None:
return chunk
startTime = time.time()
chunkData = self._chunkDataCache(cx, cz, dimName)
chunk = WorldEditorChunk(chunkData, self)
duration = time.time() - startTime
if duration > 1:
log.warn("Chunk %s took %0.2f seconds to load! entities=%s tileentities=%s tileticks=%s",
(cx, cz), duration, len(chunk.Entities), len(chunk.TileEntities),
len(chunk.rootTag.get("TileTicks", ())))
self._loadedChunks[cx, cz, dimName] = chunk
self.recentChunks.append(chunk)
return chunk
# --- Chunk dirty bit ---
def listDirtyChunks(self):
for cx, cz, dimName in self._chunkDataCache:
chunkData = self._chunkDataCache(cx, cz, dimName)
if chunkData.dirty:
yield cx, cz, dimName
# --- HeightMaps ---
def heightMapAt(self, x, z, dimName):
zc = z >> 4
xc = x >> 4
xInChunk = x & 0xf
zInChunk = z & 0xf
ch = self.getChunk(xc, zc, dimName)
heightMap = ch.HeightMap
return heightMap[zInChunk, xInChunk] # HeightMap indices are backwards
# --- Chunk manipulation ---
def containsChunk(self, cx, cz, dimName):
if self._allChunks is not None:
return (cx, cz) in self._allChunks[dimName]
if (cx, cz, dimName) in self._chunkDataCache:
return True
return self.adapter.containsChunk(cx, cz, dimName)
def containsPoint(self, x, y, z, dimName):
if y < 0 or y > 127:
return False
return self.containsChunk(x >> 4, z >> 4, dimName)
def createChunk(self, cx, cz, dimName):
if self.containsChunk(cx, cz, dimName):
raise ValueError("%r:Chunk %s already present in %s!".format(self, (cx, cz), dimName))
if hasattr(self.adapter, 'createChunk'):
if self._allChunks is not None:
self._allChunks[dimName].add((cx, cz))
chunk = self.adapter.createChunk(cx, cz, dimName)
self._chunkDataCache.store(chunk, cx, cz, dimName)
def deleteChunk(self, cx, cz, dimName):
self.adapter.deleteChunk(cx, cz, dimName)
if self._allChunks is not None:
self._allChunks[dimName].discard((cx, cz))
self._chunkDataCache.decache(cx, cz, dimName)
chunk = None
for c in self.recentChunks:
if c.chunkPosition == (cx, cz) and c.dimName == dimName:
chunk = c
break
if chunk:
self.recentChunks.remove(chunk)
# --- World metadata ---
def getWorldMetadata(self):
"""
Return an object containing global info about the world.
Different level formats can return different objects for the world metadata.
At the very least, you can expect the object to have Spawn and Seed attributes.
Currently, only AnvilWorldMetadata is ever returned.
:return:
"""
return self.adapter.metadata
def getWorldVersionInfo(self):
""" Returns a named tuple indicating the latest version of Minecraft that has played this world.
The named tuple will have the following fields:
format: The string "java" for Java Edition worlds.
id: The Minecraft build number. This is the definitive version number for this world file. Example versions:
184: version 1.9.4
922: version 1.11.2
1457: snapshot 17w50a
name: A human-readable version string. Used only to display the version number in world lists.
snapshot: Boolean. Whether this version is a prerelease.
Note that this only indicates the latest version of the game that has played the world. It is possible that
some chunks have not been touched by this version and have data structures from an older version.
"""
return self.adapter.getWorldVersionInfo()
# --- Maps ---
def listMaps(self):
"""
Return a list of map IDs for this world's map items.
:return:
"""
return self.adapter.listMaps()
def getMap(self, mapID):
"""
Return a map object for the given map ID
:param mapID: Map ID returned by listMaps
:return:
"""
return self.adapter.getMap(mapID)
def createMap(self):
return self.adapter.createMap()
def deleteMap(self, mapID):
return self.adapter.deleteMap(mapID)
# --- Players ---
def listPlayers(self):
if hasattr(self.adapter, 'listPlayers'):
return self.adapter.listPlayers()
else:
return []
def getPlayer(self, playerUUID=""):
player = self.playerCache.get(playerUUID)
if player is None:
player = self.adapter.getPlayer(playerUUID)
self.playerCache[playerUUID] = player
return player
def createPlayer(self, playerName):
return self.adapter.createPlayer(playerName)
# --- Dimensions ---
def listDimensions(self):
"""
Return a list of dimension names in this world. The name of the overworld
or the default dimension is an empty string and will always be in the list.
Returns
-------
dimNames : list[unicode]
"""
return self.adapter.listDimensions()
def getDimension(self, dimName=""):
"""
Return the dimension with the given name.
"DIM1" is The End, and "DIM-1" is The Nether. When called with an empty string or
with no arguments, returns the overworld. Some level formats may have no
dimensions other than the overworld - to get the default dimensions, call
with no arguments.
Parameters
----------
dimName : unicode
Returns
-------
dimension: WorldEditorDimension
"""
dim = self.dimensions.get(dimName)
if dim is None:
dim = WorldEditorDimension(self, dimName)
self.dimensions[dimName] = dim
return dim
def dimNameFromNumber(self, dimNo):
"""
Return the dimension name for the given number, as would be stored in the player's "dimension" tag.
Handles "DIM1" and "DIM-1" for vanilla dimensions. Most mods add more dimensions similar to "DIM-42", "DIM-100"
but some mods like Galacticraft use "DIM_SPACESTATION3" so make an educated guess about the dimension's name
ending with its number.
:param dimNo:
:type dimNo:
:return:
:rtype:
"""
dimNoStr = str(dimNo)
for name in self.listDimensions():
if name.endswith(dimNoStr):
return name
def dimNumberFromName(self, dimName):
if dimName == "":
return 0
matches = re.findall(r'-?[0-9]+', dimName)
if not len(matches):
raise ValueError("Could not parse a dimension number from %s", dimName)
return int(matches[-1])
# --- Entity Creation ---
def createEntity(self, entityID):
"""
Create a new EntityRef subclass matching the given entity ID.
If no subclass matches, return None.
Does not add the EntityRef to this world.
:param entityID:
:return:
"""
ref = self.adapter.EntityRef.create(entityID)
ref.parent = self # make blockTypes available for item IDs
return ref
@property
def hasLights(self):
return self.adapter.hasLights
class WorldEditorDimension(object):
def __init__(self, worldEditor, dimName):
"""
Parameters
----------
worldEditor : WorldEditor
dimName : unicode
"""
self.worldEditor = worldEditor
self.adapter = worldEditor.adapter
self.dimName = dimName
def __repr__(self):
return "WorldEditorDimension(dimName=%r, adapter=%r)" % (self.dimName, self.adapter)
@property
def hasLights(self):
return self.adapter.hasLights
@property
def hasSkyLight(self):
return self.dimName not in ("DIM1", "DIM-1")
# --- Bounds ---
_bounds = None
@property
def bounds(self):
"""
:return:
:rtype: BoundingBox
"""
if self._bounds is None:
if hasattr(self.adapter, "getDimensionBounds"):
self._bounds = self.adapter.getDimensionBounds(self.dimName)
else:
self._bounds = self.getWorldBounds()
return self._bounds
def getWorldBounds(self):
chunkPositions = list(self.chunkPositions())
if len(chunkPositions) == 0:
return BoundingBox((0, 0, 0), (0, 0, 0))
chunkPositions = numpy.array(chunkPositions)
mincx = (chunkPositions[:, 0]).min()
maxcx = (chunkPositions[:, 0]).max()
mincz = (chunkPositions[:, 1]).min()
maxcz = (chunkPositions[:, 1]).max()
origin = (mincx << 4, 0, mincz << 4)
size = ((maxcx - mincx + 1) << 4, self.worldEditor.maxHeight, (maxcz - mincz + 1) << 4)
return BoundingBox(origin, size)
@property
def size(self):
return self.bounds.size
@property
def blocktypes(self):
return self.worldEditor.blocktypes
# --- Chunks ---
def chunkCount(self):
return self.worldEditor.chunkCount(self.dimName)
def chunkPositions(self):
return self.worldEditor.chunkPositions(self.dimName)
def containsChunk(self, cx, cz):
return self.worldEditor.containsChunk(cx, cz, self.dimName)
def getChunk(self, cx, cz, create=False):
"""
Return the WorldEditorChunk at the given position. If create is True and the
chunk is not present, creates the chunk, otherwise raises ChunkNotPresent.
Parameters
----------
cx : int
cz : int
create : bool
Returns
-------
chunk : WorldEditorChunk
"""
return self.worldEditor.getChunk(cx, cz, self.dimName, create)
def getChunks(self, chunkPositions=None, create=False):
"""
Return an iterator over the chunks in the list of given positions.
Parameters
----------
chunkPositions: list[(int, int)]
Returns
-------
chunks : Iterator[WorldEditorChunk]
"""
if chunkPositions is None:
chunkPositions = self.chunkPositions()
for cx, cz in chunkPositions:
if self.containsChunk(cx, cz) or create:
yield self.getChunk(cx, cz, create)
def createChunk(self, cx, cz):
return self.worldEditor.createChunk(cx, cz, self.dimName)
def deleteChunk(self, cx, cz):
self.worldEditor.deleteChunk(cx, cz, self.dimName)
@property
def dimNo(self):
return self.worldEditor.dimNumberFromName(self.dimName)
# --- Entities and TileEntities ---
[docs] def getEntities(self, selection, **kw):
"""
Iterate through all entities within the given selection. If any keyword arguments
are passed, only yields those entities whose attributes match the given keywords.
For example, to iterate through only the zombies in the selection:
for entity in dimension.getEntities(selection, id="Zombie"):
# do stuff
Parameters
----------
selection : SelectionBox
kw : Entity attributes to match exactly.
Returns
-------
entities: Iterator[EntityRef]
"""
for chunk in self.getChunks(selection.chunkPositions()):
for ref in chunk.Entities:
if ref.Position in selection:
if matchEntityTags(ref, kw):
yield ref
def getTileEntities(self, selection, **kw):
for chunk in self.getChunks(selection.chunkPositions()):
for ref in chunk.TileEntities:
if ref.Position in selection:
if matchEntityTags(ref, kw):
yield ref
def getTileEntity(self, pos, **kw):
cx = pos[0] >> 4
cz = pos[2] >> 4
chunk = self.getChunk(cx, cz)
for ref in chunk.TileEntities:
if ref.Position == pos:
if matchEntityTags(ref, kw):
return ref
def addEntity(self, ref):
x, y, z = ref.Position
cx, cz = chunk_pos(x, z)
chunk = self.getChunk(cx, cz, create=True)
chunk.addEntity(ref.copy())
def addTileEntity(self, ref):
x, y, z = ref.Position
cx, cz = chunk_pos(x, z)
chunk = self.getChunk(cx, cz, create=True)
existing = [old for old in chunk.TileEntities
if old.Position == (x, y, z)]
for e in existing:
chunk.removeTileEntity(e)
chunk.addTileEntity(ref.copy())
def removeEntity(self, ref):
if ref.chunk is None:
return
ref.chunk.removeEntity(ref)
def removeTileEntity(self, ref):
if ref.chunk is None:
return
ref.chunk.removeTileEntity(ref)
# --- Import/Export ---
def copyBlocksIter(self, *a, **kw):
return copyBlocksIter(self, *a, **kw)
def copyBlocks(self, *a, **kw):
return exhaust(self.copyBlocksIter(*a, **kw))
def exportSchematicIter(self, selection):
schematic = createSchematic(shape=selection.size, blocktypes=self.blocktypes)
return itertools.chain(copyBlocksIter(schematic.getDimension(), self, selection, (0, 0, 0)), [schematic])
def exportSchematic(self, selection):
"""
:type selection: mceditlib.box.BoundingBox
:return:
:rtype: WorldEditor
"""
return exhaust(self.exportSchematicIter(selection))
def importSchematicIter(self, schematic, destPoint):
if hasattr(schematic, 'getDimension'):
# accept either WorldEditor or WorldEditorDimension
dim = schematic.getDimension()
else:
dim = schematic
return copyBlocksIter(self, dim, dim.bounds, destPoint, biomes=True, create=True)
def importSchematic(self, schematic, destPoint):
return self.importSchematicIter(schematic, destPoint)
# --- Fill/Replace ---
def fillBlocksIter(self, box, block, blocksToReplace=(), updateLights=True):
return FillBlocksOperation(self, box, block, blocksToReplace, updateLights)
def fillBlocks(self, box, block, blocksToReplace=(), updateLights=True):
return exhaust(self.fillBlocksIter(box, block, blocksToReplace, updateLights))
# --- Analyze ---
def analyzeIter(self, selection):
return AnalyzeOperation(self, selection)
# --- Blocks by single coordinate ---
[docs] def getBlock(self, x, y, z):
"""
Returns the block at the given position as an instance of BlockType.
This instance will have `id`, `meta`, and `internalName` attributes
that uniquely identify the block's type, and will have further attributes
describing the block's properties. See :ref:`BlockType` for a full description.
If the given position is outside the generated area of the world, the
`minecraft:air` BlockType will be returned.
Parameters
----------
x : int
y : int
z : int
Returns
-------
block: BlockType
"""
ID = self.getBlockID(x, y, z)
meta = self.getBlockData(x, y, z)
return self.blocktypes[ID, meta]
[docs] def setBlock(self, x, y, z, blocktype):
"""
Changes the block at the given position. The `blocktype` argument
may be either a BlockType instance, a textual identifier, a tuple containing
a textual identifier and a block metadata value, or a tuple containing
a block ID number and a block metadata value.
This function will change both the ID value and metadata value at the given position.
It is recommended to pass either a BlockType instance or a textual identifier
for readability and compatibility.
Parameters
----------
x : int
y : int
z : int
blocktype: BlockType | str | (str, int) | (int, int)
Returns
-------
block: BlockType
"""
if not isinstance(blocktype, BlockType):
blocktype = self.blocktypes[blocktype]
self.setBlockID(x, y, z, blocktype.ID)
self.setBlockData(x, y, z, blocktype.meta)
def getBlockID(self, x, y, z, default=0):
"""
Return the numeric block ID at the given position. If the position is outside
the world's generated area, returns the given default value instead.
Parameters
----------
x : int
y : int
z : int
default : int
Returns
-------
id : int
"""
cx = x >> 4
cy = y >> 4
cz = z >> 4
if self.containsChunk(cx, cz):
chunk = self.getChunk(cx, cz)
sec = chunk.getSection(cy)
if sec:
array = sec.Blocks
if array is not None:
return array[y & 0xf, z & 0xf, x & 0xf]
return default
def setBlockID(self, x, y, z, value):
"""
Changes the numeric block ID at the given position.
Parameters
----------
x : int
y : int
z : int
value : int
"""
cx = x >> 4
cy = y >> 4
cz = z >> 4
if self.containsChunk(cx, cz):
chunk = self.getChunk(cx, cz)
sec = chunk.getSection(cy, create=True)
if sec:
array = sec.Blocks
assert array is not None
if array is not None:
array[y & 0xf, z & 0xf, x & 0xf] = value
chunk.dirty = True
def getBlockData(self, x, y, z, default=0):
"""
Return the block metadata value at the given position. If the position is outside
the world's generated area, returns the given default value instead.
Parameters
----------
x : int
y : int
z : int
default : int
Returns
-------
metadata : int
"""
cx = x >> 4
cy = y >> 4
cz = z >> 4
if self.containsChunk(cx, cz):
chunk = self.getChunk(cx, cz)
sec = chunk.getSection(cy)
if sec:
array = sec.Data
if array is not None:
return array[y & 0xf, z & 0xf, x & 0xf]
return default
def setBlockData(self, x, y, z, value):
"""
Changes the block metadata value at the given position. The value must be between
0 and 15 inclusive.
Parameters
----------
x : int
y : int
z : int
value : int
"""
cx = x >> 4
cy = y >> 4
cz = z >> 4
if self.containsChunk(cx, cz):
chunk = self.getChunk(cx, cz)
sec = chunk.getSection(cy, create=True)
if sec:
array = sec.Data
assert array is not None
if array is not None:
array[y & 0xf, z & 0xf, x & 0xf] = value
chunk.dirty = True
def getLight(self, arrayName, x, y, z, default=0):
cx = x >> 4
cy = y >> 4
cz = z >> 4
if self.containsChunk(cx, cz):
chunk = self.getChunk(cx, cz)
sec = chunk.getSection(cy)
if sec:
array = getattr(sec, arrayName)
if array is not None:
return array[y & 0xf, z & 0xf, x & 0xf]
return default
def setLight(self, arrayName, x, y, z, value):
cx = x >> 4
cy = y >> 4
cz = z >> 4
if self.containsChunk(cx, cz):
chunk = self.getChunk(cx, cz)
sec = chunk.getSection(cy, create=True)
if sec:
array = getattr(sec, arrayName)
if array is not None:
array[y & 0xf, z & 0xf, x & 0xf] = value
chunk.dirty = True
def getBlockLight(self, x, y, z, default=0):
return self.getLight("BlockLight", x, y, z, default)
def setBlockLight(self, x, y, z, value):
return self.setLight("BlockLight", x, y, z, value)
def getSkyLight(self, x, y, z, default=0):
return self.getLight("SkyLight", x, y, z, default)
def setSkyLight(self, x, y, z, value):
return self.setLight("SkyLight", x, y, z, value)
def getBiomeID(self, x, z, default=0):
cx = x >> 4
cz = z >> 4
if self.containsChunk(cx, cz):
chunk = self.getChunk(cx, cz)
array = chunk.Biomes
if array is not None:
return array[z & 0xf, x & 0xf]
return default
def setBiomeID(self, x, z, value):
cx = x >> 4
cz = z >> 4
if self.containsChunk(cx, cz):
chunk = self.getChunk(cx, cz)
array = chunk.Biomes
assert array is not None
if array is not None:
array[z & 0xf, x & 0xf] = value
chunk.dirty = True
# --- Blocks by coordinate arrays ---
[docs] def getBlocks(self, x, y, z,
return_Blocks=True,
return_Data=False,
return_BlockLight=False,
return_SkyLight=False,
return_Biomes=False):
return getBlocks(self, x, y, z,
return_Blocks,
return_Data,
return_BlockLight,
return_SkyLight,
return_Biomes)
[docs] def setBlocks(self, x, y, z,
Blocks=None,
Data=None,
BlockLight=None,
SkyLight=None,
Biomes=None,
updateLights=True):
return setBlocks(self, x, y, z,
Blocks,
Data,
BlockLight,
SkyLight,
Biomes,
updateLights)