For storage, the current version of Minecraft breaks the world up into 512×512 cell regions and breaks regions up into 16×16 cell chunks. (These are the X×Z dimensions, regions and chunks are both a full 128 cells tall in the Y dimension.) Previous versions stored each chunk as a separate NBT file, but recent versions bundle up a whole region of chunks into a single MCR file. We can use this NBT parser for Python, but (as far as I can tell) it doesn't know anything about MCR files.
MCR files are really just containers for compressed NBT files. The format is described here: Beta Level Format. We can use that information to seek to the compressed chunk data, decompress it, then feed the decompressed data to the NBT parser.
def read_nbt_from_mcr_file(mcrfile, x, z): """ Read NBT chunk (x,z) from the mcrfile. 0 <= x < 32 0 <= z < 32 """ #read metadata block block = 4*(x+z*32) mcrfile.seek(block) offset, length = unpack(">IB", "\0"+mcrfile.read(4)) if offset: mcrfile.seek(offset*4096) bytecount, compression_type = unpack( ">IB", mcrfile.read(5)) data = mcrfile.read(bytecount-1) decompressed = decompress(data) nbtfile = NBTFile(buffer=StringIO(decompressed)) return nbtfile else: return None
To better manipulate this data, we're going to put it in a numpy array. We use a 3D "structured array" with fields for each of the four pieces of data we've identified. The class VolumeFactory provides various methods that create volumes:
class VolumeFactory(object): def empty_volume(self, dimensions): data = numpy.zeros( dimensions, dtype = [ ('blocks', 'u1'), ('data', 'u1'), ('skylight', 'u1'), ('blocklight', 'u1')]) return Volume(data)
def load_chunk(self, nbtfile, volume=None): if volume is not None: if volume.dimensions != (16,16,128): raise TypeError( "load_chunk requires a volume "+ "that is 16x16x128") if nbtfile is None: if volume is None: return self.empty_volume((16,16,128)) volume.blocks[:,:,:]=0 volume.skylight[:,:,:]=0 volume.blocklight[:,:,:]=0 volume.data[:,:,:]=0 return volume if volume is None: volume = self.empty_volume((16,16,128)) level = nbtfile['Level'] blocks = arrange_8bit(level['Blocks'].value) skylight = arrange_4bit(level['SkyLight'].value) blocklight = arrange_4bit(level['BlockLight'].value) data = arrange_4bit(level['Data'].value) volume.blocks[:, :, :] = blocks volume.skylight[:, :, :] = skylight volume.blocklight[:, :, :] = blocklight volume.data[:, :, :] = data return volume
def load_region(self, fname): f = open(fname, "rb") region = self.empty_volume((512,512,128)) chunk = self.empty_volume((16,16,128)) for z in xrange(32): for x in xrange(32): chunkdata = read_nbt_from_mcr_file(f, x, z) self.load_chunk(chunkdata, volume=chunk) region[16*x:16*(x+1), 16*z:16*(z+1), :] = chunk return region
The main program is largely unchanged from last week's zooming and panning one. All that is changed is how we fill the "minecraft_map" texture:
volume_factory = VolumeFactory() region_volume = volume_factory.load_region( 'world/region/r.0.0.mcr') map_rgb_array = numpy.zeros((512,512,3), dtype="u8") map_rgb_array[:,:,0] = region_volume.blocks[:,:,70] minecraft_map = make_surface(map_rgb_array)
As before, the full code is up on github. The new files are "minecraft_mapping.py" and "nbt_demo.py", the latter of which can be run. You'll need to edit it though, because the path to the Minecraft region file is hard-coded and you'll want to point it towards one of your own map files.
On Friday, we'll look at applying Minecraft's own textures to the map, and next week we'll get on to flattening the 3D map data into something (moderately) useful for a map.
This is exactly what I was looking for. As a Minecraft newbie, and trying to improve my Python (at work), this is the ideal project. Thanks for posting.
ReplyDeleteHi, I'm working on a python project using nbt as well.
ReplyDeleteI've created a script that can scan my entire \players\ folder, returning a list of all the players carrying a certain item.
I'm trying to write a similar script that will do the same for all chests on the map.
I would be using these on my server as an emergency "Who the hell has TnT?!?!?" tool.
I think the region.py file in NBT knows about the .mcr files doesn't it?
ReplyDelete