From 469319feee63679b1dc11f1baf0438f96ee226d4 Mon Sep 17 00:00:00 2001 From: David Vierra Date: Tue, 23 Aug 2011 03:27:22 -1000 Subject: [PATCH] added a daring terrain generator that calls on minecraft_server.jar, stores multiple versions of the server in an app support folder - each in a folder named with the version number (can be used to store modded servers too), and caches the results of a version/seed combo in the temp folder. also provides a clearWorldCache method to delete the generation results and automatically moves unfoldered minecraft_server*.jar files found in the server versions folder into named folders by reading the version info from the server output --- infiniteworld.py | 290 +++++++++++++++++++++++++++++++++++++++++++++++ tests.py | 15 +++ 2 files changed, 305 insertions(+) diff --git a/infiniteworld.py b/infiniteworld.py index 7c93b95..2cb18fa 100644 --- a/infiniteworld.py +++ b/infiniteworld.py @@ -9,6 +9,10 @@ import time import zlib import struct import shutil +import subprocess +import sys +import urllib +import tempfile #infinite Level = 'Level' @@ -34,6 +38,288 @@ Player = 'Player' __all__ = ["ZeroChunk", "InfdevChunk", "MCInfdevOldLevel", "MCAlphaDimension", "ZipSchematic"] +import re + +convert = lambda text: int(text) if text.isdigit() else text +alphanum_key = lambda key: [ convert(c) for c in re.split('([0-9]+)', key) ] +def sort_nicely(l): + """ Sort the given list in the way that humans expect. + """ + l.sort(key=alphanum_key) + +# Thank you, Stackoverflow +# http://stackoverflow.com/questions/377017/test-if-executable-exists-in-python +def which(program): + def is_exe(fpath): + return os.path.exists(fpath) and os.access(fpath, os.X_OK) + + fpath, _fname = os.path.split(program) + if fpath: + if is_exe(program): + return program + else: + for path in os.environ["PATH"].split(os.pathsep): + exe_file = os.path.join(path, program) + if is_exe(exe_file): + return exe_file + + return None + +if sys.platform == "win32": + appSupportDir = os.path.join(os.environ["APPDATA"], "pymclevel") +elif sys.platform == "darwin": + appSupportDir = os.path.expanduser("~/Library/Application Support/pymclevel/") +else: + appSupportDir = os.path.expanduser("~/.pymclevel") + +class ServerJarCache(object): + + def __init__(self, cacheDir=None): + if cacheDir is None: + cacheDir = os.path.join(appSupportDir, "ServerJarCache") + self.cacheDir = cacheDir + if not os.path.exists(self.cacheDir): + os.makedirs(self.cacheDir) + + cacheDirList = os.listdir(self.cacheDir) + self.versions = [v for v in cacheDirList if os.path.exists(self.jarfileForVersion(v))] + + for f in cacheDirList: + p = os.path.join(self.cacheDir, f) + if f.startswith("minecraft_server") and f.endswith(".jar") and os.path.isfile(p): + print "Unclassified minecraft_server.jar found in cache dir. Discovering version number..." + self.cacheNewVersion(p) + os.remove(p) + + + print "Minecraft_Server.jar cache initialized." + print "Each server is stored in a subdirectory of {0} named with the server's version number".format(self.cacheDir) + + + print "Cached servers: ", self.versions + + + def downloadCurrentServer(self): + print "Downloading the latest Minecraft Server..." + try: + (filename, headers) = urllib.urlretrieve("http://www.minecraft.net/download/minecraft_server.jar") + except Exception, e: + print "Error downloading server: {0!r}".format(e) + return + + self.cacheNewVersion(filename) + + def cacheNewVersion(self, filename): + """ Finds the version number from the server jar at filename and copies + it into the proper subfolder of the server jar cache folder""" + + version = MCServerChunkGenerator._serverVersionFromJarFile(filename) + print "Found version ", version + versionDir = os.path.join(self.cacheDir, version) + if not os.path.exists(versionDir): + os.mkdir(versionDir) + + shutil.copy2(filename, os.path.join(versionDir, "minecraft_server.jar")) + + if version not in self.versions: + self.versions.append(version) + + def jarfileForVersion(self, v): + return os.path.join(self.cacheDir, v, "minecraft_server.jar") + + @property + def latestVersion(self): + if len(self.versions) == 0: return None + return max(self.versions, key=alphanum_key) + + def getJarfile(self, version=None): + version = version or self.latestVersion + if len(self.versions) == 0: + print "No servers found in cache." + self.downloadCurrentServer() + + if version not in self.versions: return None + return self.jarfileForVersion(version) + +class JavaNotFound(RuntimeError): pass +class VersionNotFound(RuntimeError): pass + +class MCServerChunkGenerator(object): + """Generates chunks using minecraft_server.jar. Uses a ServerJarCache to + store different versions of minecraft_server.jar in an application support + folder. + + + + from pymclevel import * + + Example usage: + + gen = MCServerChunkGenerator() # with no arguments, use the newest + # server version in the cache, or download + # the newest one automatically + level = loadWorldNamed("MyWorld") + + gen.generateChunkInLevel(level, 12, 24) + + + Using an older version: + + gen = MCServerChunkGenerator("Beta 1.6.5") + + """ + + if sys.platform == "win32": + javaExe = which("java.exe") + else: + javaExe = which("java") + + jarcache = None + + def __init__(self, version=None, jarfile=None): + if self.__class__.jarcache is None: + self.__class__.jarcache = ServerJarCache() + if self.javaExe is None: + raise JavaNotFound, "Could not find java. Please check that java is installed correctly. (Could not find java in your PATH environment variable.)" + if jarfile is None: + jarfile = self.jarcache.getJarfile(version) + if jarfile is None: + raise VersionNotFound, "Could not find minecraft_server.jar for version {0}. Please make sure that a minecraft_server.jar is placed under {1} in a subfolder named after the server's version number.".format(version or "(latest)", self.jarcache.cacheDir) + self.serverJarFile = jarfile + self.serverVersion = self._serverVersion() + self.tempWorldCache = {} + + def clearWorldCache(self): + tempDirs = [tempDir for tempDir, _tempWorld in self.tempWorldCache.itervalues()] + #self.tempWorldCache = None + + for tempDir in tempDirs: + shutil.rmtree(tempDir) + + def waitForServer(self, proc): + """ wait for the server to finish starting up, then stop it. """ + while proc.poll() is None: + line = proc.stderr.readline() + if "[INFO] Done" in line: + proc.stdin.write("stop\n") + proc.wait() + break + if "FAILED TO BIND" in line: + proc.kill() + proc.wait() + raise RuntimeError, "Server Died!" + + def tempWorldForLevel(self, level): + if level.RandomSeed in self.tempWorldCache: + return self.tempWorldCache[level.RandomSeed] + + #tempDir = tempfile.mkdtemp("mclevel_servergen") + tempDir = os.path.join(tempfile.gettempdir(), "pymclevel_MCServerChunkGenerator", self.serverVersion, str(level.RandomSeed)) + if not os.path.exists(tempDir): + os.makedirs(tempDir) + + tempWorldDir = os.path.join(tempDir, "world") + tempWorld = MCInfdevOldLevel(tempWorldDir, create=True, random_seed=level.RandomSeed) + self.tempWorldCache[level.RandomSeed] = (tempWorld, tempDir) + return (tempWorld, tempDir) + + def generateChunkInLevel(self, level, cx, cz): + assert isinstance(level, MCInfdevOldLevel) + tempWorld, tempDir = self.tempWorldForLevel(level) + self.generateAtPosition(tempWorld, tempDir, cx, cz) + self.copyChunkAtPosition(tempWorld, level, cx, cz) + + def generateAtPosition(self, tempWorld, tempDir, cx, cz): + tempWorld.setPlayerSpawnPosition((cx * 16, 64, cz * 16)) + tempWorld.saveInPlace() + tempWorld.unloadRegions() + + proc = self.runServer(tempDir) + self.waitForServer(proc) + + def copyChunkAtPosition(self, tempWorld, level, cx, cz): + tempChunk = tempWorld.getChunk(cx, cz) + tempChunk.decompress() + tempChunk.unpackChunkData() + root_tag = tempChunk.root_tag + + if not level.containsChunk(cx, cz): + level.createChunk(cx, cz) + + chunk = level.getChunk(cx, cz) + chunk.decompress() + chunk.unpackChunkData() + chunk.root_tag = root_tag #xxx tag swap, could copy blocks and entities and chunk attrs instead? + chunk.dirty = True + + chunk.compress() + tempChunk.compress() + + + def generateChunksInLevel(self, level, chunks): + assert isinstance(level, MCInfdevOldLevel) + tempWorld, tempDir = self.tempWorldForLevel(level) + + def inBox(cPos): + x, z = cPos + return x > centercx - 12 and x < centercx + 12 and z > centercz - 12 and z < centercz + 12 + + while len(chunks): + centercx, centercz = chunks[0] + + boxedChunks = [cPos for cPos in chunks if inBox(cPos)] + print "Generating {0} chunks out of {1} starting from {2}".format(len(boxedChunks), len(chunks), (centercx, centercz)) + chunks = [c for c in chunks if not inBox(c)] + + self.generateAtPosition(tempWorld, tempDir, centercx, centercz) + for cx, cz in boxedChunks: + self.copyChunkAtPosition(tempWorld, level, cx, cz) + + level.saveInPlace() + + def runServer(self, startingDir): + return self._runServer(startingDir, self.serverJarFile) + + @classmethod + def _runServer(cls, startingDir, jarfile): + print "Starting server {0} in {1}".format(jarfile, startingDir) + proc = subprocess.Popen([cls.javaExe, "-jar", jarfile], + executable=cls.javaExe, + cwd=startingDir, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + ) + return proc + + def _serverVersion(self): + return self._serverVersionFromJarFile(self.serverJarFile) + + @classmethod + def _serverVersionFromJarFile(cls, jarfile): + tempdir = tempfile.mkdtemp("mclevel_servergen") + proc = cls._runServer(tempdir, jarfile) + + version = "Unknown" + #out, err = proc.communicate() + #for line in err.split("\n"): + + while True: + line = proc.stderr.readline() + if "Preparing start region" in line: break + if "Starting minecraft server version" in line: + version = line.split("Starting minecraft server version")[1].strip() + break + + + proc.kill() + proc.wait() + shutil.rmtree(tempdir) + + return version + + class ZeroChunk(object): " a placebo for neighboring-chunk routines " def compress(self): pass @@ -1132,6 +1418,10 @@ class MCInfdevOldLevel(EntityLevel): self.regionFiles[rx, rz] = regionFile; return regionFile + def unloadRegions(self): + self.regionFiles = {} + self._allChunks = None + def preloadRegions(self): info(u"Scanning for regions...") self._allChunks = set() diff --git a/tests.py b/tests.py index 373a5da..24b1bba 100644 --- a/tests.py +++ b/tests.py @@ -18,6 +18,7 @@ import os import numpy from numpy import * from logging import info +from pymclevel.infiniteworld import MCServerChunkGenerator #logging.basicConfig(format=u'%(levelname)s:%(message)s') #logging.getLogger().level = logging.INFO @@ -245,6 +246,20 @@ class TestSchematics(unittest.TestCase): info("TileEntities: ", invFile.TileEntities) #raise SystemExit; +class TestServerGen(unittest.TestCase): + def setUp(self): + #self.alphaLevel = TempLevel("Dojo_64_64_128.dat") + self.alphalevel = TempLevel("PyTestWorld") + + def testServerGen(self): + gen = MCServerChunkGenerator() + print "Version: ", gen.serverVersion + + level = self.alphalevel.level + + gen.generateChunkInLevel(level, 50, 50) + gen.generateChunksInLevel(level, [(120, 50), (121, 50), (122, 50), (123, 50), (244, 244), (244, 245), (244, 246)]) + if __name__ == "__main__": #import sys;sys.argv = ['', 'Test.testName'] unittest.main()