# Requires at least Python 2.5, PIL, numpy, nbt # Minecraft editor used to lower a world by a certain amount of blocks # By: Nathan Viniconis # # You can use this code freely without any obligation to the original or myself # assumes http://www.minecraftwiki.net/wiki/Alpha_Level_Format/Chunk_File_Format still holds import ctypes import nbt import numpy import os.path import sys import glob from nbt import * # Lowers a world by a certain number of blocks. This will give more room for massive # sculptures, allowing for a more detailed stop-action animation. ####################### # Editable globals ####################### # toggles logging to console verbose = True # path to the MineCraft save directory pathToWorld = "C:\Users\Revrick\AppData\Roaming\.minecraft\saves\World2" # number of blocks to lower the world by numBlocksToLowerWorld = 40 # used to disable saving for testing purposes testCode = True ####################### # Global Inits ####################### # number of blocks processed numProcessed = 0 # used to store empty entities which are then used to clear all mobs/chests/etc from every # chunk emptyEntities = None emptyTileEntities = None # ///////////////////////////////////////////////////////////////////////////// # updates the time to noon in a minecraft save # ///////////////////////////////////////////////////////////////////////////// def updateTimeToNoon(): # create the file name for the level.dat pathToDat = pathToWorld + "\level.dat" # get the nbfile if( os.path.isfile(pathToDat) == False): print pathToDat + " does not exist." return 0 try: nb = nbt.NBTFile(pathToDat) except Exception,e: print "could not open nbt file " + pathToDat + str(e) # 0 sunrise, 6000 noon, 12000 sunset, 18000 midnight, 24000 nb["Data"]["Time"].value = 6000 # save nb.write_file() # ///////////////////////////////////////////////////////////////////////////// # Lower the player so that he does not fall to his death on login # after lowering the world, a death appears to make you spawn somewhere near sea level. # this causes large areas of land to be generated and can cause lag/crashes. # ///////////////////////////////////////////////////////////////////////////// def lowerPlayer(): # create the file name for the level.dat pathToDat = pathToWorld + "\level.dat" # get the nbfile if( os.path.isfile(pathToDat) == False): print "Can not lower player." return 0 try: nb = nbt.NBTFile(pathToDat) except Exception,e: print "could not open nbt file " + pathToDat + str(e) # get the old value and lower Ypos = nb["Data"]["Player"]["Pos"].tags[1] Ypos.value -= float(numBlocksToLowerWorld) if (Ypos <= 0): Ypos = float(127) # have fun falling! hahaha # set it and save nb["Data"]["Player"]["Pos"].tags[1] = Ypos nb.write_file() # ///////////////////////////////////////////////////////////////////////////// # loads a NBTFile for a given chunk # ///////////////////////////////////////////////////////////////////////////// def getNBTFileFromChunk(chunkPath): nb = None if( os.path.isfile(chunkPath) == False): print chunkPath + " does not exist." return nb try: nb = nbt.NBTFile(chunkPath) except Exception,e: print "could not open nbt file " + chunkPath + str(e) blockIDs = numpy.fromstring(nb["Level"]["Blocks"].value, dtype='uint8') blockData = numpy.fromstring(nb["Level"]["Data"].value, dtype='uint8') heightMap = numpy.fromstring(nb["Level"]["HeightMap"].value, dtype='uint8') skyLight = numpy.fromstring(nb["Level"]["SkyLight"].value, dtype='uint8') blockLight = numpy.fromstring(nb["Level"]["BlockLight"].value, dtype='uint8') return {"NBTFile":nb,"Blocks":blockIDs,"Data":blockData, \ "HeightMap":heightMap,"SkyLight":skyLight, \ "BlockLight":blockLight} # ///////////////////////////////////////////////////////////////////////////// # Saves the blocks data back into the minecraft chunk save file # ///////////////////////////////////////////////////////////////////////////// def saveDataForChunk(chunkData): # make sure the blockIDs is valid if (len(chunkData["Blocks"]) != 32768): print "Not saving, blockID length of: " + str(len(chunkData[1])) return # make sure blockData is valid if (len(chunkData["Data"]) != 16384): print "Not saving: blockData length of: " + str(len(chunkData[2])) return # calculate the new heightmap for the chunk if (len(chunkData["HeightMap"]) != 256): print "Not saving: heightMap length of: " + str(len(chunkData[3])) # TODO: Calc skylight, blocklight, and anything else important # replace the data in the streams chunkData["NBTFile"]["Level"].__getitem__("Blocks").value = numpy.getbuffer(chunkData["Blocks"]) chunkData["NBTFile"]["Level"].__getitem__("Data").value = numpy.getbuffer(chunkData["Data"]) chunkData["NBTFile"]["Level"].__getitem__("HeightMap").value = numpy.getbuffer(chunkData["HeightMap"]) chunkData["NBTFile"]["Level"].__getitem__("SkyLight").value = numpy.getbuffer(chunkData["SkyLight"]) chunkData["NBTFile"]["Level"].__getitem__("BlockLight").value = numpy.getbuffer(chunkData["BlockLight"]) # replace entities and tileEntities with the empty versions chunkData["NBTFile"]["Level"].__setitem__("Entities", emptyEntities) chunkData["NBTFile"]["Level"].__setitem__("TileEntities", emptyTileEntities) # actually save the file chunkData["NBTFile"].write_file() # ///////////////////////////////////////////////////////////////////////////// # Extract half of a byte, used to get info out of the 4-bit constructs # ///////////////////////////////////////////////////////////////////////////// def extractHalfByte(fullByte, isLowerHalf): if (isLowerHalf): return 0x0F & fullByte return (fullByte >> 4) # ///////////////////////////////////////////////////////////////////////////// # Set half of a byte to a certain 4-bit value. Pass in if it is the lower half or not # ///////////////////////////////////////////////////////////////////////////// def setHalfByte(fullByte, newHalfByte, isLowerHalf): # clear and set the lower half if (isLowerHalf): return (fullByte & 0xF0) | newHalfByte # clear and set the upper half return (fullByte & 0x0F) | (newHalfByte << 4) # ///////////////////////////////////////////////////////////////////////////// # Wrapper that sets a 4-bit property in a byte array # ///////////////////////////////////////////////////////////////////////////// def set4bitProperty(blockIndex, halfByteBuffer, newData): # determine the index of the full byte the half byte is a part of byteIndex = int(blockIndex/2) lowerHalfOfByte = (blockIndex % 2 == 0) # get the original full byte from the buffer try: origByte = halfByteBuffer[byteIndex] except: print "BlockID:" + str(blockIndex) + " is out of bounds.." print "Type: " + str(type(halfByteBuffer)) + " Size: " + str(len(halfByteBuffer)) sys.exit(0) # set half the byte to the new value newByte = setHalfByte(origByte, newData, lowerHalfOfByte) halfByteBuffer[byteIndex] = newByte # ///////////////////////////////////////////////////////////////////////////// # Wrapper that gets a 4-bit property in a byte array # ///////////////////////////////////////////////////////////////////////////// def get4bitProperty(blockIndex, halfByteBuffer): byteIndex = int(blockIndex/2) lowerHalfOfByte = (blockIndex % 2 == 0) origByte = halfByteBuffer[byteIndex] return extractHalfByte(origByte, lowerHalfOfByte) # ///////////////////////////////////////////////////////////////////////////// # Copy all the data from one block to another. If clearOrig is true, clear the original # ///////////////////////////////////////////////////////////////////////////// def moveBlockDataFromTo(chunkData, origID, targetID, clearOrig): global numProcessed numProcessed += 1 # Move the BlockIDs origBlockID = chunkData["Blocks"][origID] chunkData["Blocks"][targetID] = origBlockID # Move the Data origData = get4bitProperty(origID, chunkData["Data"]) set4bitProperty(targetID, chunkData["Data"], origData) # Move the Skylight info origData = get4bitProperty(origID, chunkData["SkyLight"]) set4bitProperty(targetID, chunkData["SkyLight"], origData) # Move the BlockLight info (skip this as it seems trivial and only takes up time) #origData = get4bitProperty(origID, chunkData["BlockLight"]) #set4bitProperty(targetID, chunkData["BlockLight"], origData) if (clearOrig): # clear block to air chunkData["Blocks"][origID] = 0 # clear any data for the block (water flowing, tnt, that kind of stuff) set4bitProperty(origID, chunkData["Data"], 0) # clear the sky light for air, make 15 since it has full visibility set4bitProperty(origID, chunkData["SkyLight"], 15) # clear the block light for air set4bitProperty(origID, chunkData["BlockLight"], 0) # ///////////////////////////////////////////////////////////////////////////// # Find an instance of empty entities so that it can be copied for every chunk # Not sure how this will work if the world is only lowered a few so the entities # like chests, mob spawners, etc still exist.... use at your own risk! hah # ///////////////////////////////////////////////////////////////////////////// def getEmptyEntities(chunkData): global emptyEntities global emptyTileEntities entities = chunkData["NBTFile"]["Level"]["Entities"] tileEntities = chunkData["NBTFile"]["Level"]["TileEntities"] if (emptyEntities == None and entities.value == None): emptyEntities = entities if (emptyTileEntities == None and tileEntities.value == None): emptyTileEntities = tileEntities # ///////////////////////////////////////////////////////////////////////////// # Given the chunk, process the data so that it is lowered # ///////////////////////////////////////////////////////////////////////////// def lowerChunk(chunkData): # The height goes from 0 to 127. Sea level is 63. If you lower by more than 63, there will be probs if (numBlocksToLowerWorld >= 63): print "Lowering the world by more than sea-level will cause probs.. falling and all.." print "If you're SURE you know what you're doing, remove this check.." sys.exit(0) # Go through every block, lowering its info for Yb in range(128): # since the blocks that are being pushed off the world are going to be cleared, skip them if (Yb < numBlocksToLowerWorld): continue; # if this is at the top of the world where nothing is taking it's place, clear it clearOrig = False if (Yb > 127-numBlocksToLowerWorld): clearOrig = True for Xb in range(16): for Zb in range(16): targetYb = Yb - numBlocksToLowerWorld # find the ID of the block origID = Yb + ( Zb * 128 + ( Xb * 128 * 16) ) targetID = targetYb + ( Zb * 128 + ( Xb * 128 * 16) ) # Copy the data from the original to the target moveBlockDataFromTo(chunkData, origID, targetID, clearOrig) # Update the HeightMap # go through each Z,X and lower by 'numBlocksToLowerWorld' for Xb in range(16): for Zb in range(16): index = [Xb * 16 + Zb] oldHeight = chunkData["HeightMap"][index] newHeight = oldHeight - numBlocksToLowerWorld if (newHeight < 0): newHeight = 0 chunkData["HeightMap"][index] = newHeight return True # ///////////////////////////////////////////////////////////////////////////// # Main controller, do the necessary processing # ///////////////////////////////////////////////////////////////////////////// def main(): # get every chunk for the world chunks = glob.glob(pathToWorld + "/*/*/*.dat") if (testCode): print "** TEST ** - Chunk Updates not saved!" # go through the chunks and file some copies of the empty entities so # we can replace all the other chunks with them to erase them if (verbose): print "Finding empty Entities..." for chunk in chunks: if (emptyEntities != None or emptyTileEntities != None): break chunkData = getNBTFileFromChunk(chunk) getEmptyEntities(chunkData) # go through every chunk and lower it for chunk in chunks: # get the NBTFile for the chunk print "Processing: " + chunk chunkData = getNBTFileFromChunk(chunk) # do the lowering result = lowerChunk(chunkData) # save if it worked and not testing if (result == True and testCode == False): saveDataForChunk(chunkData) if (result == False and verbose == True): print "Error lowering chunk: " + chunk print "Num blocks processed: " + str(numProcessed) updateTimeToNoon() # assume the player is in the areas being lowered.. If this isn't true, have fun dying! if (testCode == False): lowerPlayer() if __name__ == "__main__": main()