diff --git a/mce.py b/mce.py index c6e3538..7612e6d 100755 --- a/mce.py +++ b/mce.py @@ -11,6 +11,8 @@ class BlockMatchError(RuntimeError): pass class PlayerNotFound(RuntimeError): pass class mce(object): + random_seed = os.getenv('MCE_RANDOM_SEED', None) + last_played = os.getenv("MCE_LAST_PLAYED", None) """ Usage: @@ -195,6 +197,7 @@ class mce(object): def _debug(self, command): self.debug = not self.debug print "Debug", ("disabled", "enabled")[self.debug] + def _log(self, command): """ log [ ] @@ -369,7 +372,7 @@ class mce(object): filename = command.pop(0) destPoint = self.readPoint(command) - importLevel = mclevel.fromFile(filename) + importLevel = mclevel.fromFile(filename, last_played=self.last_played, random_seed=self.random_seed) self.level.copyBlocksFrom(importLevel, importLevel.getWorldBounds(), destPoint); @@ -652,7 +655,7 @@ class mce(object): self.loadWorld(command[0]) def _reload(self, command): - self.level = mclevel.fromFile(self.filename); + self.level = mclevel.fromFile(self.filename, last_played=self.last_played, random_seed=self.random_seed); def _help(self, command): if len(command): @@ -706,7 +709,7 @@ class mce(object): try: worldNum = int(world) except ValueError: - self.level = mclevel.fromFile(world) + self.level = mclevel.fromFile(world, last_played=self.last_played, random_seed=self.random_seed) self.filename = self.level.filename @@ -814,6 +817,7 @@ try: except Exception, e: traceback.print_exc() print e + raise SystemExit(1) #editor.printUsage() diff --git a/mclevel.py b/mclevel.py index 6df482c..f6ec6d1 100644 --- a/mclevel.py +++ b/mclevel.py @@ -587,7 +587,7 @@ class MCLevel(object): def saveInPlace(self): self.saveToFile(self.filename); @classmethod - def fromFile(cls, filename, loadInfinite=True): + def fromFile(cls, filename, loadInfinite=True, random_seed=None, last_played=None): ''' The preferred method for loading Minecraft levels of any type. pass False to loadInfinite if you'd rather not load infdev levels.''' info( "Identifying " + filename ) @@ -604,7 +604,7 @@ class MCLevel(object): raise; try: info( "Can't read, attempting to open directory" ) - lev = MCInfdevOldLevel(filename=filename) + lev = MCInfdevOldLevel(filename=filename, random_seed=random_seed, last_played=last_played) info( "Detected Alpha world." ) return lev; except Exception, ex: @@ -1494,7 +1494,7 @@ class MCInfdevOldLevel(MCLevel): def __str__(self): return "MCInfdevOldLevel(" + os.path.split(self.worldDir)[1] + ")" - def __init__(self, filename = None, root_tag = None): + def __init__(self, filename = None, root_tag = None, random_seed=None, last_played=None): #pass level.dat's root tag and filename to read an existing level. #pass only filename to create a new one #filename should be the path to the world dir @@ -1521,8 +1521,13 @@ class MCInfdevOldLevel(MCLevel): root_tag[Data][SpawnY] = TAG_Int(2) root_tag[Data][SpawnZ] = TAG_Int(0) - root_tag[Data]['LastPlayed'] = TAG_Long(long(time.time())) - root_tag[Data]['RandomSeed'] = TAG_Long(int(random.random() * ((2<<31)))) + if last_played is None: + last_played = time.time() + if random_seed is None: + random_seed = random.random() * ((2<<31)) + + root_tag[Data]['LastPlayed'] = TAG_Long(long(last_played)) + root_tag[Data]['RandomSeed'] = TAG_Long(int(random_seed)) root_tag[Data]['SizeOnDisk'] = TAG_Long(long(1048576)) root_tag[Data]['Time'] = TAG_Long(1) root_tag[Data]['SnowCovered'] = TAG_Byte(0); @@ -1627,18 +1632,18 @@ class MCInfdevOldLevel(MCLevel): def base36(self, n): n = int(n); if 0 == n: return '0' - s = ""; neg = ""; if n < 0: neg = "-" n = -n; + work = [] + while(n): - digit = n % 36; - n /= 36 - s=self.base36alphabet[digit]+s + n, digit = divmod(n, 36) + work.append(self.base36alphabet[digit]) - return neg + s; + return neg + ''.join(reversed(work)) def dirhashlookup(self, n): return self.dirhashes[n%64]; diff --git a/nbt.py b/nbt.py index ba9c2a4..db02678 100644 --- a/nbt.py +++ b/nbt.py @@ -227,13 +227,8 @@ class TAG_Compound(TAG_Value, collections.MutableMapping): assert_type(tag_type, data_cursor) - tag_name = TAG_String( data=data[data_cursor:] ) - data_cursor += tag_name.nbt_length() - tag_name = tag_name.value + data_cursor, tag = load_named(data, data_cursor, tag_type) - tag = tag_handlers[tag_type]( data=data[data_cursor:], name=tag_name ) - - data_cursor += tag.nbt_length() self.value.append(tag); @@ -389,6 +384,15 @@ def loadFile(filename): else: return load(buf=fromstring(data, 'uint8')); +def load_named(data, data_cursor, tag_type): + tag_name = TAG_String( data=data[data_cursor:] ) + data_cursor += tag_name.nbt_length() + tag_name = tag_name.value + + tag = tag_handlers[tag_type]( data=data[data_cursor:], name=tag_name) + data_cursor += tag.nbt_length() + return data_cursor, tag + def load(filename="", buf = None): """Unserialize data from an entire NBT file and return the root TAG_Compound object. Argument can be a string containing a @@ -408,12 +412,7 @@ def load(filename="", buf = None): raise IOError, 'Not an NBT file with a root TAG_Compound (found {0})'.format(tag_type); data_cursor += 1; - tag_name = TAG_String( data=data[data_cursor:] ) - data_cursor += tag_name.nbt_length() - tag_name = tag_name.value - - tag = tag_handlers[tag_type]( data=data[data_cursor:]) - tag.name = tag_name; + data_cursor, tag = load_named(data, data_cursor, tag_type) return tag; diff --git a/regression_test/alpha.tar.gz b/regression_test/alpha.tar.gz new file mode 100644 index 0000000..771e31c Binary files /dev/null and b/regression_test/alpha.tar.gz differ diff --git a/run_regression_test.py b/run_regression_test.py new file mode 100755 index 0000000..97c2aae --- /dev/null +++ b/run_regression_test.py @@ -0,0 +1,156 @@ +#!/usr/bin/env python + +import tempfile +import sys +import subprocess +import shutil +import os +import mclevel +import hashlib +import contextlib +import gzip +import fnmatch +import tarfile +import zipfile + +def generate_file_list(directory): + for dirpath, dirnames, filenames in os.walk(directory): + for filename in filenames: + yield os.path.join(dirpath, filename) + +def sha1_file(name, checksum=None): + CHUNKSIZE=1024 + if checksum is None: + checksum = hashlib.sha1() + if fnmatch.fnmatch(name, "*.dat"): + opener = gzip.open + else: + opener = open + + with contextlib.closing(opener(name, 'rb')) as data: + chunk = data.read(CHUNKSIZE) + while len(chunk) == CHUNKSIZE: + checksum.update(chunk) + chunk = data.read(CHUNKSIZE) + else: + checksum.update(chunk) + return checksum + +def calculate_result(directory): + checksum = hashlib.sha1() + for filename in sorted(generate_file_list(directory)): + sha1_file(filename, checksum) + return checksum.hexdigest() + +@contextlib.contextmanager +def temporary_directory(prefix='regr'): + name = tempfile.mkdtemp(prefix) + try: + yield name + finally: + shutil.rmtree(name) + +@contextlib.contextmanager +def directory_clone(src): + with temporary_directory('regr') as name: + subdir = os.path.join(name, "subdir") + shutil.copytree(src, subdir) + yield subdir + +@contextlib.contextmanager +def unzipped_content(src): + with temporary_directory() as dest: + f = zipfile.ZipFile.open(name) + f.extractall(dest) + yield dest + +@contextlib.contextmanager +def untared_content(src): + with temporary_directory() as dest: + f = tarfile.TarFile.open(src) + f.extractall(dest) + yield dest + +class RegressionError(Exception): pass + +def do_test(test_data, result_check, arguments=[]): + """Run a regression test on the given world. + + result_check - sha1 of the recursive tree generated + arguments - arguments to give to mce.py on execution + """ + result_check = result_check.lower() + + env = { + 'MCE_RANDOM_SEED' : '42', + 'MCE_LAST_PLAYED' : '42' + } + + with directory_clone(test_data) as directory: + proc = subprocess.Popen([ + "./mce.py", + directory] + arguments, stdin=subprocess.PIPE, stderr=subprocess.STDOUT, env=env) + proc.stdin.close() + result = proc.wait() + + if os.WIFEXITED(result) and os.WEXITSTATUS(result): + raise RegressionError("Program execution failed!") + + checksum = calculate_result(directory).lower() + if checksum != result_check.lower(): + raise RegressionError("Checksum mismatch: {0!r} != {1!r}".format(checksum, result_check)) + print "[OK] (sha1sum of result is {0!r}, as expected)".format(result_check) + + +def do_test_match_output(test_data, result_check, arguments=[]): + result_check = result_check.lower() + + env = { + 'MCE_RANDOM_SEED' : '42', + 'MCE_LAST_PLAYED' : '42' + } + + with directory_clone(test_data) as directory: + proc = subprocess.Popen([ + "./mce.py", + directory] + arguments, stdin=subprocess.PIPE, stdout=subprocess.PIPE, env=env) + proc.stdin.close() + output = proc.stdout.read() + result = proc.wait() + + if os.WIFEXITED(result) and os.WEXITSTATUS(result): + raise RegressionError("Program execution failed!") + + checksum = hashlib.sha1() + checksum.update(output) + checksum = checksum.hexdigest() + if checksum != result_check.lower(): + raise RegressionError("Checksum mismatch: {0!r} != {1!r}".format(checksum, result_check)) + print "[OK] (sha1sum of result is {0!r}, as expected)".format(result_check) + + +alpha_tests = [ + (do_test, 'baseline', 'ca66277d8037fde5aea3a135dd186f91e4bf4bef', []), + (do_test, 'degrief', '6ae14eceab8e0c600799463a77113448b2d9ff8c', ['degrief']), + (do_test_match_output, 'analyze', 'f2938515596b88509b2e4c8d598951887d7e0f4c', ['analyze']), + (do_test, 'relight', '00bc507daa3c07fee065973da4b81a099124650f', ['relight']), + (do_test, 'replace', 'b26c3d3c05dd873fd8fd29b6b7a38e3ebd9a3e8e', ['replace', 'Water', 'with', 'Lava']), + (do_test, 'fill', 'f9dd5d49789b4c7363bf55eab03b05846e89f89f', ['fill', 'Water']), +] + +def main(argv): + if len(argv) <= 1: + do_these_regressions = ['*'] + else: + do_these_regressions = argv[1:] + + with untared_content("regression_test/alpha.tar.gz") as directory: + test_data = os.path.join(directory, "alpha") + for func, name, sha, args in alpha_tests: + if any(fnmatch.fnmatch(name, x) for x in do_these_regressions): + func(test_data, sha, args) + print "Regression {0!r} complete.".format(name) + +if __name__ == '__main__': + sys.exit(main(sys.argv)) +