Skip to content

Instantly share code, notes, and snippets.

@DanielKeep
Created November 19, 2010 15:03
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save DanielKeep/706617 to your computer and use it in GitHub Desktop.
Save DanielKeep/706617 to your computer and use it in GitHub Desktop.
Minecraft World Translater
#
# Minecraft World Translater
# Copyright (C) 2010, Daniel Keep.
# Licensed under the BSD <http://www.opensource.org/licenses/bsd-license.php>
# Canonical URL: https://gist.github.com/706617
#
# Usage: mctranslate.py X-OFFSET Z-OFFSET
#
# Translates a Minecraft world by (X-OFFSET,Z-OFFSET) chunks and places the
# result in the "translated" directory.
#
##
## nbt.py
## https://github.com/twoolie/NBT/blob/master/nbt/nbt.py
##
## Included here verbatim so this doesn't require more than one source file. :P
##
## Copyright (c) 2010 Thomas Woolford
##
## Permission is hereby granted, free of charge, to any person obtaining a copy
## of this software and associated documentation files (the "Software"), to deal
## in the Software without restriction, including without limitation the rights
## to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
## copies of the Software, and to permit persons to whom the Software is
## furnished to do so, subject to the following conditions:
##
## The above copyright notice and this permission notice shall be included in
## all copies or substantial portions of the Software.
##
## THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
## IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
## FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
## AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
## LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
## OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
## THE SOFTWARE.
##
## BEGIN
from struct import pack, unpack, calcsize
from gzip import GzipFile
TAG_END = 0
TAG_BYTE = 1
TAG_SHORT = 2
TAG_INT = 3
TAG_LONG = 4
TAG_FLOAT = 5
TAG_DOUBLE = 6
TAG_BYTE_ARRAY = 7
TAG_STRING = 8
TAG_LIST = 9
TAG_COMPOUND = 10
class TAG(object):
"""Each Tag needs to take a file-like object for reading and writing.
The file object will be initialised by the calling code."""
id = None
def __init__(self, value=None, name=None):
self.name = name
self.value = value
#Parsers and Generators
def _parse_buffer(self, buffer):
raise NotImplementedError(self.__class__.__name__)
def _render_buffer(self, buffer, offset=None):
raise NotImplementedError(self.__class__.__name__)
#Printing and Formatting of tree
def tag_info(self):
return self.__class__.__name__ + \
('(%s)' % repr(self.name)) + \
": " + self.__repr__()
def pretty_tree(self, indent=0):
return ("\t"*indent) + self.tag_info()
class _TAG_Numeric(TAG):
def __init__(self, value=None, name=None, buffer=None):
super(_TAG_Numeric, self).__init__(value, name)
self.size = calcsize(self.fmt)
if buffer:
self._parse_buffer(buffer)
#Parsers and Generators
def _parse_buffer(self, buffer, offset=None):
self.value = unpack(self.fmt, buffer.read(self.size))[0]
def _render_buffer(self, buffer, offset=None):
buffer.write(pack(self.fmt, self.value))
#Printing and Formatting of tree
def __repr__(self):
return str(self.value)
#== Value Tags ==#
class TAG_Byte(_TAG_Numeric):
id = TAG_BYTE
fmt = ">b"
class TAG_Short(_TAG_Numeric):
id = TAG_SHORT
fmt = ">h"
class TAG_Int(_TAG_Numeric):
id = TAG_INT
fmt = ">i"
class TAG_Long(_TAG_Numeric):
id = TAG_LONG
fmt = ">q"
class TAG_Float(_TAG_Numeric):
id = TAG_FLOAT
fmt = ">f"
class TAG_Double(_TAG_Numeric):
id = TAG_DOUBLE
fmt = ">d"
class TAG_Byte_Array(TAG):
id = TAG_BYTE_ARRAY
def __init__(self, buffer=None):
super(TAG_Byte_Array, self).__init__()
self.value = ''
if buffer:
self._parse_buffer(buffer)
#Parsers and Generators
def _parse_buffer(self, buffer, offset=None):
length = TAG_Int(buffer=buffer)
self.value = buffer.read(length.value)
def _render_buffer(self, buffer, offset=None):
length = TAG_Int(len(self.value))
length._render_buffer(buffer, offset)
buffer.write(self.value)
#Printing and Formatting of tree
def __repr__(self):
return "[%i bytes]" % len(self.value)
class TAG_String(TAG):
id = TAG_STRING
def __init__(self, value=None, name=None, buffer=None):
super(TAG_String, self).__init__(value, name)
if buffer:
self._parse_buffer(buffer)
#Parsers and Generators
def _parse_buffer(self, buffer, offset=None):
length = TAG_Short(buffer=buffer)
self.value = unicode(buffer.read(length.value), "utf-8")
def _render_buffer(self, buffer, offset=None):
save_val = self.value.encode("utf-8")
length = TAG_Short(len(save_val))
length._render_buffer(buffer, offset)
buffer.write(save_val)
#Printing and Formatting of tree
def __repr__(self):
return self.value
#== Collection Tags ==#
class TAG_List(TAG):
id = TAG_LIST
def __init__(self, type=None, value=None, name=None, buffer=None):
super(TAG_List, self).__init__(value, name)
if type:
self.tagID = type.id
else: self.tagID = None
self.tags = []
if buffer:
self._parse_buffer(buffer)
if not self.tagID:
raise ValueError("No type specified for list")
#Parsers and Generators
def _parse_buffer(self, buffer, offset=None):
self.tagID = TAG_Byte(buffer=buffer).value
self.tags = []
length = TAG_Int(buffer=buffer)
for x in range(length.value):
self.tags.append(TAGLIST[self.tagID](buffer=buffer))
def _render_buffer(self, buffer, offset=None):
TAG_Byte(self.tagID)._render_buffer(buffer, offset)
length = TAG_Int(len(self.tags))
length._render_buffer(buffer, offset)
for i, tag in enumerate(self.tags):
if tag.id != self.tagID:
raise ValueError("List element %d(%s) has type %d != container type %d" %
(i, tag, tag.id, self.tagID))
tag._render_buffer(buffer, offset)
#Printing and Formatting of tree
def __repr__(self):
return "%i entries of type %s" % (len(self.tags), TAGLIST[self.tagID].__name__)
def pretty_tree(self, indent=0):
output = [super(TAG_List,self).pretty_tree(indent)]
if len(self.tags):
output.append(("\t"*indent) + "{")
output.extend([tag.pretty_tree(indent+1) for tag in self.tags])
output.append(("\t"*indent) + "}")
return '\n'.join(output)
class TAG_Compound(TAG):
id = TAG_COMPOUND
def __init__(self, buffer=None):
super(TAG_Compound, self).__init__()
self.tags = []
if buffer:
self._parse_buffer(buffer)
#Parsers and Generators
def _parse_buffer(self, buffer, offset=None):
while True:
type = TAG_Byte(buffer=buffer)
if type.value == TAG_END:
#print "found tag_end"
break
else:
name = TAG_String(buffer=buffer).value
try:
#DEBUG print type, name
tag = TAGLIST[type.value](buffer=buffer)
tag.name = name
self.tags.append(tag)
except KeyError:
raise ValueError("Unrecognised tag type")
def _render_buffer(self, buffer, offset=None):
for tag in self.tags:
TAG_Byte(tag.id)._render_buffer(buffer, offset)
TAG_String(tag.name)._render_buffer(buffer, offset)
tag._render_buffer(buffer,offset)
buffer.write('\x00') #write TAG_END
#Accessors
def __getitem__(self, key):
if isinstance(key,int):
return self.tags[key]
elif isinstance(key, str):
for tag in self.tags:
if tag.name == key:
return tag
else:
raise KeyError("A tag with this name does not exist")
else:
raise ValueError("key needs to be either name of tag, or index of tag")
def __setitem__(self, key, value):
if isinstance(key, int):
# Just try it. The proper error will be raised if it doesn't work.
self.tags[key] = value
elif isinstance(key, str):
value.name = key
for i, tag in enumerate(self.tags):
if tag.name == key:
self.tags[i] = value
return
self.tags.append(value)
#Printing and Formatting of tree
def __repr__(self):
return '%i Entries' % len(self.tags)
def pretty_tree(self, indent=0):
output = [super(TAG_Compound,self).pretty_tree(indent)]
if len(self.tags):
output.append(("\t"*indent) + "{")
output.extend([tag.pretty_tree(indent+1) for tag in self.tags])
output.append(("\t"*indent) + "}")
return '\n'.join(output)
TAGLIST = {TAG_BYTE:TAG_Byte, TAG_SHORT:TAG_Short, TAG_INT:TAG_Int, TAG_LONG:TAG_Long, TAG_FLOAT:TAG_Float, TAG_DOUBLE:TAG_Double, TAG_BYTE_ARRAY:TAG_Byte_Array, TAG_STRING:TAG_String, TAG_LIST:TAG_List, TAG_COMPOUND:TAG_Compound}
class NBTFile(TAG_Compound):
"""Represents an NBT file object"""
def __init__(self, filename=None, mode=None, buffer=None):
super(NBTFile,self).__init__()
self.__class__.__name__ = "TAG_Compound"
self.filename = filename
self.type = TAG_Byte(self.id)
if filename:
self.file = GzipFile(filename, mode)
elif buffer:
self.file = buffer
else:
self.file = None
#parse the file given intitially
if self.file:
self.parse_file(self.file)
if filename and 'close' in dir(self.file):
self.file.close()
self.file = None
def parse_file(self, file=None):
if not file:
file = self.file
if file:
type = TAG_Byte(buffer=file)
if type.value == self.id:
name = TAG_String(buffer=file).value
self._parse_buffer(file)
self.name = name
self.file.close()
else:
raise ValueError("First record is not a Compound Tag")
else: ValueError("need a file!")
def write_file(self, filename=None, buffer=None):
# EDIT: DanielKeep
# Closes files it opens now
close = False
if buffer:
self.file = buffer
elif filename:
self.file = GzipFile(filename, "wb")
close = True
elif self.filename:
self.file = GzipFile(self.filename, "wb")
close = True
elif not self.file:
raise ValueError("Need to specify either a filename or a file")
#Render tree to file
TAG_Byte(self.id)._render_buffer(self.file)
TAG_String(self.name)._render_buffer(self.file)
self._render_buffer(self.file)
if close:
self.file.close()
## END
import os
import os.path
import re
import shutil
import sys
opj = os.path.join
BASE36 = '0123456789abcdefghijklmnopqrstuvwxyz'
RE_CHUNK = re.compile(r'c\.(-?[0-9a-z]+)\.(-?[0-9a-z]+)\.dat$')
def base36(n):
if n == 0: return '0'
neg = (n < 0)
r = ''
n = abs(n)
while n > 0:
i = n % 36
n = n / 36
r = BASE36[i] + r
return ('-' if neg else '') + r
def unbase36(n):
return int(n,36)
assert base36(51) == '1f'
assert base36(44) == '18'
assert base36(-13) == '-d'
assert unbase36('1f') == 51
assert unbase36('18') == 44
assert unbase36('-d') == -13
def chunk_pos_to_path((x,z)):
return opj(base36(x % 64), base36(z % 64),
"".join(['c.', base36(x), '.', base36(z), '.dat']))
def chunk_path_to_pos(path):
x,z = RE_CHUNK.search(path).groups()
return unbase36(x), unbase36(z)
def main(exe, args):
if len(args) != 2:
print 'Usage: %s X-OFFSET Z-OFFSET' % exe
print 'Shifts the chunks in the current world by (X-OFFSET,Z-OFFSET)'
return
if not os.path.isfile('level.dat'):
print "Error: current directory doesn't appear to be a Minecraft world."
print " Make sure to run this program from the directory containing level.dat"
return 1
x,z = int(args[0]), int(args[1])
print 'Translating world by (%d,%d)...' % (x,z)
if not os.path.exists('translated'):
os.mkdir('translated')
for root,dirs,files in os.walk('.'):
if 'translated' in dirs:
dirs.remove('translated')
for file in files:
if RE_CHUNK.search(file):
cx,cz = chunk_path_to_pos(file)
cx += x
cz += z
prefix = os.path.dirname(file)
for i in range(2):
prefix = os.path.dirname(prefix)
path = opj('translated',prefix,chunk_pos_to_path((cx,cz)))
fdir,fname = os.path.split(path)
print ' %s -> %s' % (opj(root,file), path)
try:
os.makedirs(fdir)
except:
pass
nbt = NBTFile(opj(root,file))
nbt['Level']['xPos'].value = cx
nbt['Level']['zPos'].value = cz
nbt.write_file(filename=path)
if __name__ == '__main__' and not __file__.endswith('idle.pyw'):
sys.exit(main(os.path.split(__file__)[1], sys.argv[1:]))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment