Switch branches/tags
Nothing to show
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
587 lines (441 sloc) 18.5 KB
Created on Jul 22, 2011
@author: Rio
from mclevelbase import *
import shutil
from level import MCLevel, EntityLevel
log = logging.getLogger(__name__)
warn, error, info, debug = log.warn, log.error,, log.debug
Materials = 'Materials'
__all__ = ['MCSchematic', 'INVEditChest']
class MCSchematic (EntityLevel):
materials = alphaMaterials
def __init__(self, shape=None, root_tag=None, filename=None, mats='Alpha'):
""" shape is (x,y,z) for a new level's shape. if none, takes
root_tag as a TAG_Compound for an existing schematic file. if
none, tries to read the tag from filename. if none, results
are undefined. materials can be a MCMaterials instance, or one of
"Classic", "Alpha", "Pocket" to indicate allowable blocks. The default
is Alpha.
block coordinate order in the file is y,z,x to use the same code as classic/indev levels.
in hindsight, this was a completely arbitrary decision.
the Entities and TileEntities are nbt.TAG_List objects containing TAG_Compounds.
this makes it easy to copy entities without knowing about their insides.
rotateLeft swaps the axes of the different arrays. because of this, the Width, Height, and Length
reflect the current dimensions of the schematic rather than the ones specified in the NBT structure.
I'm not sure what happens when I try to re-save a rotated schematic.
#if(shape != None):
# self.setShape(shape)
if filename:
self.filename = filename
if None is root_tag and os.path.exists(filename):
root_tag = nbt.load(filename)
self.filename = None
if mats in namedMaterials:
self.materials = namedMaterials[mats]
assert(isinstance(mats, MCMaterials))
self.materials = mats
if root_tag:
self.root_tag = root_tag
if Materials in root_tag:
self.materials = namedMaterials[self.Materials]
root_tag[Materials] = TAG_String(
assert shape != None
root_tag = TAG_Compound(name="Schematic")
root_tag[Height] = TAG_Short(shape[1])
root_tag[Length] = TAG_Short(shape[2])
root_tag[Width] = TAG_Short(shape[0])
root_tag[Entities] = TAG_List()
root_tag[TileEntities] = TAG_List()
root_tag["Materials"] = TAG_String(
root_tag[Blocks] = TAG_Byte_Array(zeros((shape[1], shape[2], shape[0]), uint8))
root_tag[Data] = TAG_Byte_Array(zeros((shape[1], shape[2], shape[0]), uint8))
self.root_tag = root_tag
self.dataIsPacked = True
def __str__(self):
return u"MCSchematic(shape={0}, materials={2}, filename=\"{1}\")".format(self.size, self.filename or u"", self.Materials)
def compress(self):
#if self.root_tag is not None, then our compressed data must be stale and we need to recompress.
if self.root_tag is None:
buf = StringIO()
with closing(gzip.GzipFile(fileobj=buf, mode='wb', compresslevel=2)) as gzipper:
self.compressedTag = buf.getvalue()
self.root_tag = None
def decompress(self):
"""called when accessing attributes decorated with @decompress_first"""
if self.root_tag != None: return
if self.compressedTag is None:
if self.root_tag is None:
with closing(gzip.GzipFile(fileobj=StringIO(self.compressedTag))) as gzipper:
data =
if data == None: return;
except Exception, e:
#error( u"Error reading compressed data, assuming uncompressed: {0}".format(e) )
data = self.compressedTag
self.root_tag = nbt.load(buf=data)
except Exception, e:
error(u"Malformed NBT data in schematic file: {0} ({1})".format(self.filename, e))
raise ChunkMalformed, (e,self.filename), sys.exc_info()[2]
except KeyError, e:
error(u"Incorrect schematic format in file: {0} ({1})".format(self.filename, e))
raise ChunkMalformed, (e,self.filename), sys.exc_info()[2]
self.dataIsPacked = True
#these refer to the blocks array instead of the file's height because rotation swaps the axes
# this will have an impact later on when editing schematics instead of just importing/exporting
def Length(self):return self.Blocks.shape[1]
def Width(self):return self.Blocks.shape[0]
def Height(self):return self.Blocks.shape[2]
def Blocks(self):
return self.root_tag[Blocks].value
def Blocks(self, newval):
self.root_tag[Blocks].value = newval
def Data(self):
return self.root_tag[Data].value
def Data(self, newval):
self.root_tag[Data].value = newval
def Entities(self):
return self.root_tag[Entities]
def TileEntities(self):
return self.root_tag[TileEntities]
def Materials(self):
return self.root_tag[Materials].value
def Materials(self, val):
if not Materials in self.root_tag:
self.root_tag[Materials] = TAG_String()
self.root_tag[Materials].value = val
def _isTagLevel(cls, root_tag):
return "Schematic" ==
def shapeChunkData(self):
w = self.root_tag[Width].value
l = self.root_tag[Length].value
h = self.root_tag[Height].value
self.root_tag[Blocks].value.shape = (h, l, w)
self.root_tag[Data].value.shape = (h, l, w)
def packUnpack(self):
self.root_tag[Blocks].value = swapaxes(self.root_tag[Blocks].value, 0, 2)#yzx to xzy
self.root_tag[Data].value = swapaxes(self.root_tag[Data].value, 0, 2)#yzx to xzy
if self.dataIsPacked:
self.root_tag[Data].value &= 0xF #discard high bits
def packChunkData(self):
if not self.dataIsPacked:
self.dataIsPacked = True
def unpackChunkData(self):
if self.dataIsPacked:
self.dataIsPacked = False
def _update_shape(self):
shape = self.Blocks.shape
root_tag[Height] = TAG_Short(shape[2])
root_tag[Length] = TAG_Short(shape[1])
root_tag[Width] = TAG_Short(shape[0])
def rotateLeft(self):
self.Blocks = swapaxes(self.Blocks, 1, 0)[:, ::-1, :] #x=z; z=-x
self.Data = swapaxes(self.Data, 1, 0)[:, ::-1, :] #x=z; z=-x
blockrotation.RotateLeft(self.Blocks, self.Data)
info(u"Relocating entities...")
for entity in self.Entities:
for p in "Pos", "Motion":
if p == "Pos":
zBase = self.Length
zBase = 0.0
newX = entity[p][2].value
newZ = zBase - entity[p][0].value
entity[p][0].value = newX
entity[p][2].value = newZ
entity["Rotation"][0].value -= 90.0
if entity["id"].value == "Painting":
x, z = entity["TileX"].value, entity["TileZ"].value
newx = z
newz = self.Length - x - 1
entity["TileX"].value, entity["TileZ"].value = newx, newz
entity["Dir"].value = (entity["Dir"].value + 1) % 4
for tileEntity in self.TileEntities:
if not 'x' in tileEntity: continue
newX = tileEntity["z"].value
newZ = self.Length - tileEntity["x"].value - 1
tileEntity["x"].value = newX
tileEntity["z"].value = newZ
def roll(self):
" xxx rotate stuff "
self.Blocks = swapaxes(self.Blocks, 2, 0)[:, :, ::-1] #x=z; z=-x
self.Data = swapaxes(self.Data, 2, 0)[:, :, ::-1]
def flipVertical(self):
" xxx delete stuff "
blockrotation.FlipVertical(self.Blocks, self.Data)
self.Blocks = self.Blocks[:, :, ::-1] #y=-y
self.Data = self.Data[:, :, ::-1]
def flipNorthSouth(self):
blockrotation.FlipNorthSouth(self.Blocks, self.Data)
self.Blocks = self.Blocks[::-1, :, :] #x=-x
self.Data = self.Data[::-1, :, :]
northSouthPaintingMap = [0, 3, 2, 1]
info(u"N/S Flip: Relocating entities...")
for entity in self.Entities:
entity["Pos"][0].value = self.Width - entity["Pos"][0].value
entity["Motion"][0].value = -entity["Motion"][0].value
entity["Rotation"][0].value -= 180.0
if entity["id"].value == "Painting":
entity["TileX"].value = self.Width - entity["TileX"].value
entity["Dir"].value = northSouthPaintingMap[entity["Dir"].value]
for tileEntity in self.TileEntities:
if not 'x' in tileEntity: continue
tileEntity["x"].value = self.Width - tileEntity["x"].value - 1
def flipEastWest(self):
" xxx flip entities "
blockrotation.FlipEastWest(self.Blocks, self.Data)
self.Blocks = self.Blocks[:, ::-1, :] #z=-z
self.Data = self.Data[:, ::-1, :]
eastWestPaintingMap = [2, 1, 0, 3]
info(u"E/W Flip: Relocating entities...")
for entity in self.Entities:
entity["Pos"][2].value = self.Length - entity["Pos"][2].value
entity["Motion"][2].value = -entity["Motion"][2].value
entity["Rotation"][0].value -= 180.0
if entity["id"].value == "Painting":
entity["TileZ"].value = self.Length - entity["TileZ"].value
entity["Dir"].value = eastWestPaintingMap[entity["Dir"].value]
for tileEntity in self.TileEntities:
tileEntity["z"].value = self.Length - tileEntity["z"].value - 1
def setShape(self, shape):
"""shape is a tuple of (width, height, length). sets the
schematic's properties and clears the block and data arrays"""
x, y, z = shape
shape = (x, z, y)
self.root_tag[Blocks].value = zeros(dtype='uint8', shape=shape)
self.root_tag[Data].value = zeros(dtype='uint8', shape=shape)
def saveToFile(self, filename=None):
""" save to file named filename, or use self.filename. XXX NOT THREAD SAFE AT ALL. """
if filename == None: filename = self.filename
if filename == None:
warn(u"Attempted to save an unnamed schematic in place")
return #you fool!
self.Materials =
with open(filename, 'wb') as chunkfh:
def setBlockDataAt(self, x, y, z, newdata):
if x < 0 or y < 0 or z < 0: return 0
if x >= self.Width or y >= self.Height or z >= self.Length: return 0;
self.Data[x, z, y] = (newdata & 0xf)
def blockDataAt(self, x, y, z):
if x < 0 or y < 0 or z < 0: return 0
if x >= self.Width or y >= self.Height or z >= self.Length: return 0;
return self.Data[x, z, y]
def chestWithItemID(self, itemID, count=64, damage=0):
""" Creates a chest with a stack of 'itemID' in each slot.
Optionally specify the count of items in each stack. Pass a negative
value for damage to create unnaturally sturdy tools. """
root_tag = TAG_Compound()
invTag = TAG_List()
root_tag["Inventory"] = invTag
for slot in range(9, 36):
itemTag = TAG_Compound()
itemTag["Slot"] = TAG_Byte(slot)
itemTag["Count"] = TAG_Byte(count)
itemTag["id"] = TAG_Short(itemID)
itemTag["Damage"] = TAG_Short(damage)
chest = INVEditChest(root_tag, "")
return chest
class INVEditChest(MCSchematic):
Width = 1
Height = 1
Length = 1
Blocks = array([[[alphaMaterials.Chest.ID]]], 'uint8')
Data = array([[[0]]], 'uint8')
Entities = TAG_List()
Materials = alphaMaterials
def _isTagLevel(cls, root_tag):
return "Inventory" in root_tag
def __init__(self, root_tag, filename):
if filename:
self.filename = filename
if None is root_tag:
root_tag = nbt.load(filename)
except IOError, e:
info(u"Failed to load file {0}".format(e))
assert root_tag, "Must have either root_tag or filename"
self.filename = None
for item in list(root_tag["Inventory"]):
slot = item["Slot"].value
if slot < 9 or slot >= 36:
item["Slot"].value -= 9 # adjust for different chest slot indexes
self.root_tag = root_tag
def TileEntities(self):
chestTag = TAG_Compound()
chestTag["id"] = TAG_String("Chest")
chestTag["Items"] = TAG_List(self.root_tag["Inventory"])
chestTag["x"] = TAG_Int(0)
chestTag["y"] = TAG_Int(0)
chestTag["z"] = TAG_Int(0)
return TAG_List([chestTag], name="TileEntities")
def adjustExtractionParameters(self, box):
x, y, z = box.origin
w, h, l = box.size
destX = destY = destZ = 0
if y < 0:
destY -= y
h += y
y = 0
if y >= self.Height: return;
if y + h >= self.Height:
h -= y + h - self.Height
y = self.Height - h
if h <= 0: return
if self.Width:
if x < 0:
w += x
destX -= x
x = 0
if x >= self.Width: return;
if x + w >= self.Width:
w = self.Width - x
if w <= 0: return
if z < 0:
l += z
destZ -= z
z = 0
if z >= self.Length: return;
if z + l >= self.Length:
l = self.Length - z
if l <= 0: return
box = BoundingBox ((x, y, z), (w, h, l))
return box, (destX, destY, destZ)
def extractSchematicFrom(sourceLevel, box, entities=True):
return exhaust(extractSchematicFromIter(sourceLevel, box, entities))
def extractSchematicFromIter(sourceLevel, box, entities=True):
p = sourceLevel.adjustExtractionParameters(box)
if p is None:
yield None
newbox, destPoint = p
tempSchematic = MCSchematic(shape=box.size, mats=sourceLevel.materials)
for i in tempSchematic.copyBlocksFromIter(sourceLevel, newbox, destPoint, entities=entities):
yield i
yield tempSchematic
MCLevel.extractSchematic = extractSchematicFrom
MCLevel.extractSchematicIter = extractSchematicFromIter
MCLevel.adjustExtractionParameters = adjustExtractionParameters
import tempfile
def extractZipSchematicFrom(sourceLevel, box, zipfilename=None, entities=True):
return exhaust(extractZipSchematicFromIter(sourceLevel, box, zipfilename, entities))
def extractZipSchematicFromIter(sourceLevel, box, zipfilename=None, entities=True):
#converts classic blocks to alpha
#probably should only apply to alpha levels
if zipfilename is None:
zipfilename = tempfile.mktemp("zipschematic")
p = sourceLevel.adjustExtractionParameters(box)
if p is None: return
sourceBox, destPoint = p
destPoint = (0, 0, 0)
tempfolder = tempfile.mktemp("schematic")
done = False
tempSchematic = MCInfdevOldLevel(tempfolder, create=True)
tempSchematic.materials = sourceLevel.materials
destBox = BoundingBox(destPoint, sourceBox.size)
for i in tempSchematic.copyBlocksFromIter(sourceLevel, sourceBox, destPoint, entities=entities, create=True):
yield i
tempSchematic.saveInPlace() #lights not needed for this format - crashes minecraft though
schematicDat = TAG_Compound() = "Mega Schematic"
schematicDat["Width"] = TAG_Int(sourceBox.size[0])
schematicDat["Height"] = TAG_Int(sourceBox.size[1])
schematicDat["Length"] = TAG_Int(sourceBox.size[2])
schematicDat["Materials"] = TAG_String(, "schematic.dat"))
zipdir(tempfolder, zipfilename)
import mclevel
yield mclevel.fromFile(zipfilename)
#We get here if the generator is GCed also
if os.path.exists(tempfolder): shutil.rmtree(tempfolder, False)
MCLevel.extractZipSchematic = extractZipSchematicFrom
MCLevel.extractZipSchematicIter = extractZipSchematicFromIter
def extractAnySchematic(level, box):
return exhaust(level.extractAnySchematicIter(box))
def extractAnySchematicIter(level, box):
if box.chunkCount > MCInfdevOldLevel.decompressedChunkLimit:
raise MemoryError
for i in level.extractSchematicIter(box):
yield i
except MemoryError:
for i in level.extractZipSchematicIter(box):
yield i
MCLevel.extractAnySchematic = extractAnySchematic
MCLevel.extractAnySchematicIter = extractAnySchematicIter
from zipfile import ZipFile, ZIP_STORED
def zipdir(basedir, archivename):
assert os.path.isdir(basedir)
with closing(ZipFile(archivename, "w", ZIP_STORED)) as z:
for root, dirs, files in os.walk(basedir):
#NOTE: ignore empty directories
for fn in files:
absfn = os.path.join(root, fn)
zfn = absfn[len(basedir) + len(os.sep):] #XXX: relative path
z.write(absfn, zfn)
from infiniteworld import MCInfdevOldLevel