diff --git a/infiniteworld.py b/infiniteworld.py index 3a83cd5..befd6ca 100644 --- a/infiniteworld.py +++ b/infiniteworld.py @@ -4,29 +4,26 @@ Created on Jul 22, 2011 @author: Rio ''' -# **FIXME** WindowsError is the name of a built-in Exception, but pyflakes doesn't seem to know that. -zothar from contextlib import closing from datetime import datetime -from entity import Entity, TileEntity -from faces import FaceXDecreasing, FaceXIncreasing, FaceZDecreasing, FaceZIncreasing import itertools from logging import getLogger -from materials import alphaMaterials, namedMaterials from math import floor -from mclevelbase import appDataDir, ChunkMalformed, ChunkNotPresent, exhaust, PlayerNotFound -import nbt -from numpy import array, clip, maximum, zeros import os -from os.path import join, dirname, basename import random import time import traceback import zlib import shutil -import subprocess import sys import tempfile -import urllib + +from entity import Entity, TileEntity +from faces import FaceXDecreasing, FaceXIncreasing, FaceZDecreasing, FaceZIncreasing +from materials import alphaMaterials, namedMaterials +from mclevelbase import ChunkMalformed, ChunkNotPresent, exhaust, PlayerNotFound +import nbt +from numpy import array, clip, maximum, zeros from regionfile import MCRegionFile log = getLogger(__name__) @@ -34,565 +31,20 @@ warn, error, info, debug = log.warn, log.error, log.info, log.debug import blockrotation from box import BoundingBox -from level import LightedChunk, EntityLevel, computeChunkHeightMap, MCLevel - -# infinite +from level import LightedChunk, EntityLevel, computeChunkHeightMap, MCLevel, ChunkBase DIM_NETHER = -1 DIM_END = 1 __all__ = ["ZeroChunk", "AnvilChunk", "ChunkedLevelMixin", "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(f): - return os.path.exists(f) and os.access(f, os.X_OK) - - fpath, _fname = os.path.split(program) - if fpath: - if is_exe(program): - return program - else: - if sys.platform == "win32": - if "SYSTEMROOT" in os.environ: - root = os.environ["SYSTEMROOT"] - exe_file = os.path.join(root, program) - if is_exe(exe_file): - return exe_file - if "PATH" in os.environ: - 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(appDataDir, u"pymclevel") -elif sys.platform == "darwin": - appSupportDir = os.path.expanduser(u"~/Library/Application Support/pymclevel/") -else: - appSupportDir = os.path.expanduser(u"~/.pymclevel") - - -class ServerJarStorage(object): - defaultCacheDir = os.path.join(appSupportDir, u"ServerJarStorage") - - def __init__(self, cacheDir=None): - if cacheDir is None: - cacheDir = self.defaultCacheDir - - self.cacheDir = cacheDir - - if not os.path.exists(self.cacheDir): - os.makedirs(self.cacheDir) - readme = os.path.join(self.cacheDir, "README.TXT") - if not os.path.exists(readme): - with file(readme, "w") as f: - f.write(""" -About this folder: - -This folder is used by MCEdit and pymclevel to store different versions of the -Minecraft Server to use for terrain generation. It should have one or more -subfolders, one for each version of the server. Each subfolder must hold at -least one file named minecraft_server.jar, and the subfolder's name should -have the server's version plus the names of any installed mods. - -There may already be a subfolder here (for example, "Beta 1.7.3") if you have -used the Chunk Create feature in MCEdit to create chunks using the server. - -Version numbers can be automatically detected. If you place one or more -minecraft_server.jar files in this folder, they will be placed automatically -into well-named subfolders the next time you run MCEdit. If a file's name -begins with "minecraft_server" and ends with ".jar", it will be detected in -this way. -""") - - self.reloadVersions() - - def reloadVersions(self): - cacheDirList = os.listdir(self.cacheDir) - self.versions = list(reversed(sorted([v for v in cacheDirList if os.path.exists(self.jarfileForVersion(v))], key=alphanum_key))) - - if MCServerChunkGenerator.javaExe: - 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 storage initialized." - print u"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, allowDuplicate=False) - - def cacheNewVersion(self, filename, allowDuplicate=True): - """ 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) - - i = 1 - newVersionDir = versionDir - while os.path.exists(newVersionDir): - if not allowDuplicate: - return - - newVersionDir = versionDir + " (" + str(i) + ")" - i += 1 - - os.mkdir(newVersionDir) - - shutil.copy2(filename, os.path.join(newVersionDir, "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").encode(sys.getfilesystemencoding()) - - def checksumForVersion(self, v): - jf = self.jarfileForVersion(v) - with file(jf, "rb") as f: - import hashlib - return hashlib.md5(f.read()).hexdigest() - - broken_versions = ["Beta 1.9 Prerelease {0}".format(i) for i in (1, 2, 3)] - - @property - def latestVersion(self): - if len(self.versions) == 0: - return None - return max((v for v in self.versions if v not in self.broken_versions), key=alphanum_key) - - def getJarfile(self, version=None): - if len(self.versions) == 0: - print "No servers found in cache." - self.downloadCurrentServer() - - version = version or self.latestVersion - if version not in self.versions: - return None - return self.jarfileForVersion(version) - - -class JavaNotFound(RuntimeError): - pass - - -class VersionNotFound(RuntimeError): - pass - - -def readProperties(filename): - if not os.path.exists(filename): - return {} - - with file(filename) as f: - properties = dict((line.split("=", 2) for line in (l.strip() for l in f) if not line.startswith("#"))) - - return properties - - -def saveProperties(filename, properties): - with file(filename, "w") as f: - for k, v in properties.iteritems(): - f.write("{0}={1}\n".format(k, v)) - - -def findJava(): - if sys.platform == "win32": - javaExe = which("java.exe") - if javaExe is None: - KEY_NAME = "HKLM\SOFTWARE\JavaSoft\Java Runtime Environment" - try: - p = subprocess.Popen(["REG", "QUERY", KEY_NAME, "/v", "CurrentVersion"], stdout=subprocess.PIPE, universal_newlines=True) - o, e = p.communicate() - lines = o.split("\n") - for l in lines: - l = l.strip() - if l.startswith("CurrentVersion"): - words = l.split(None, 2) - version = words[-1] - p = subprocess.Popen(["REG", "QUERY", KEY_NAME + "\\" + version, "/v", "JavaHome"], stdout=subprocess.PIPE, universal_newlines=True) - o, e = p.communicate() - lines = o.split("\n") - for l in lines: - l = l.strip() - if l.startswith("JavaHome"): - w = l.split(None, 2) - javaHome = w[-1] - javaExe = os.path.join(javaHome, "bin", "java.exe") - print "RegQuery: java.exe found at ", javaExe - break - - except Exception, e: - print "Error while locating java.exe using the Registry: ", repr(e) - else: - javaExe = which("java") - - return javaExe - - -class MCServerChunkGenerator(object): - """Generates chunks using minecraft_server.jar. Uses a ServerJarStorage 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") - - """ - defaultJarStorage = None - - javaExe = findJava() - jarStorage = None - tempWorldCache = {} - - def __init__(self, version=None, jarfile=None, jarStorage=None): - - self.jarStorage = jarStorage or self.getDefaultJarStorage() - - 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.jarStorage.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.jarStorage.cacheDir)) - self.serverJarFile = jarfile - self.serverVersion = version or self._serverVersion() - - @classmethod - def getDefaultJarStorage(cls): - if cls.defaultJarStorage is None: - cls.defaultJarStorage = ServerJarStorage() - return cls.defaultJarStorage - - @classmethod - def clearWorldCache(cls): - cls.tempWorldCache = {} - - for tempDir in os.listdir(cls.worldCacheDir): - t = os.path.join(cls.worldCacheDir, tempDir) - if os.path.isdir(t): - shutil.rmtree(t) - - def createReadme(self): - readme = os.path.join(self.worldCacheDir, "README.TXT") - - if not os.path.exists(readme): - with file(readme, "w") as f: - f.write(""" - About this folder: - - This folder is used by MCEdit and pymclevel to cache levels during terrain - generation. Feel free to delete it for any reason. - """) - - worldCacheDir = os.path.join(tempfile.gettempdir(), "pymclevel_MCServerChunkGenerator") - - def tempWorldForLevel(self, level): - - # tempDir = tempfile.mkdtemp("mclevel_servergen") - tempDir = os.path.join(self.worldCacheDir, self.jarStorage.checksumForVersion(self.serverVersion), str(level.RandomSeed)) - propsFile = os.path.join(tempDir, "server.properties") - properties = readProperties(propsFile) - - tempWorld = self.tempWorldCache.get((self.serverVersion, level.RandomSeed)) - - if tempWorld is None: - if not os.path.exists(tempDir): - os.makedirs(tempDir) - self.createReadme() - - worldName = "world" - worldName = properties.setdefault("level-name", worldName) - - tempWorldDir = os.path.join(tempDir, worldName) - tempWorld = MCInfdevOldLevel(tempWorldDir, create=True, random_seed=level.RandomSeed) - del tempWorld.version # for compatibility with older servers. newer ones will set it again without issue. - - self.tempWorldCache[self.serverVersion, level.RandomSeed] = tempWorld - - if level.dimNo == 0: - properties["allow-nether"] = "false" - else: - tempWorld = tempWorld.getDimension(level.dimNo) - - properties["allow-nether"] = "true" - - properties["server-port"] = int(32767 + random.random() * 32700) - saveProperties(propsFile, properties) - - return tempWorld, tempDir - - def generateAtPosition(self, tempWorld, tempDir, cx, cz): - return exhaust(self.generateAtPositionIter(tempWorld, tempDir, cx, cz)) - - def generateAtPositionIter(self, tempWorld, tempDir, cx, cz, simulate=False): - tempWorld.setPlayerSpawnPosition((cx * 16, 64, cz * 16)) - tempWorld.saveInPlace() - tempWorld.unloadRegions() - - startTime = time.time() - proc = self.runServer(tempDir) - while proc.poll() is None: - line = proc.stderr.readline().strip() - info(line) - yield line - - if "[INFO] Done" in line: - if simulate: - duration = time.time() - startTime - - simSeconds = max(8, int(duration) + 1) - - for i in range(simSeconds): - # process tile ticks - yield "%2d/%2d: Simulating the world for a little bit..." % (i, simSeconds) - time.sleep(1) - - proc.stdin.write("stop\n") - proc.wait() - break - if "FAILED TO BIND" in line: - proc.kill() - proc.wait() - raise RuntimeError("Server failed to bind to port!") - - stdout, _ = proc.communicate() - - if "Could not reserve enough space" in stdout and not MCServerChunkGenerator.lowMemory: - MCServerChunkGenerator.lowMemory = True - for i in self.generateAtPositionIter(tempWorld, tempDir, cx, cz): - yield i - - (tempWorld.parentWorld or tempWorld).loadLevelDat() # reload version number - - def copyChunkAtPosition(self, tempWorld, level, cx, cz): - if level.containsChunk(cx, cz): - return - try: - tempChunk = tempWorld.getChunk(cx, cz) - except ChunkNotPresent, e: - raise ChunkNotPresent("While generating a world in {0} using server {1} ({2!r})".format(tempWorld, self.serverJarFile, e), sys.exc_traceback) - - if not level.containsChunk(cx, cz): - level.createChunk(cx, cz) - - chunk = level.getChunk(cx, cz) - chunk.root_tag = tempChunk.root_tag - chunk.dirty = True - - chunk.save() - - - 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) - - minRadius = 5 - maxRadius = 20 - - def createLevel(self, level, box, simulate=False, **kw): - return exhaust(self.createLevelIter(level, box, simulate, **kw)) - - def createLevelIter(self, level, box, simulate=False, **kw): - if isinstance(level, basestring): - filename = level - level = MCInfdevOldLevel(filename, create=True, **kw) - - assert isinstance(level, MCInfdevOldLevel) - minRadius = self.minRadius - - genPositions = list(itertools.product( - xrange(box.mincx, box.maxcx, minRadius * 2), - xrange(box.mincz, box.maxcz, minRadius * 2))) - - for i, (cx, cz) in enumerate(genPositions): - info("Generating at %s" % ((cx, cz),)) - parentDir = dirname(level.worldDir) - propsFile = join(parentDir, "server.properties") - props = readProperties(join(dirname(self.serverJarFile), "server.properties")) - props["level-name"] = basename(level.worldDir) - props["server-port"] = int(32767 + random.random() * 32700) - saveProperties(propsFile, props) - - for p in self.generateAtPositionIter(level, parentDir, cx, cz, simulate): - yield i, len(genPositions), p - - level.unloadRegions() - - def generateChunksInLevel(self, level, chunks): - return exhaust(self.generateChunksInLevelIter(level, chunks)) - - def generateChunksInLevelIter(self, level, chunks, simulate=False): - assert isinstance(level, MCInfdevOldLevel) - tempWorld, tempDir = self.tempWorldForLevel(level) - - startLength = len(chunks) - minRadius = self.minRadius - maxRadius = self.maxRadius - chunks = set(chunks) - - while len(chunks): - length = len(chunks) - centercx, centercz = chunks.pop() - chunks.add((centercx, centercz)) - # assume the generator always generates at least an 11x11 chunk square. - centercx += minRadius - centercz += minRadius - - # boxedChunks = [cPos for cPos in chunks if inBox(cPos)] - - print "Generating {0} chunks out of {1} starting from {2}".format("XXX", len(chunks), (centercx, centercz)) - yield startLength - len(chunks), startLength - - # chunks = [c for c in chunks if not inBox(c)] - - for p in self.generateAtPositionIter(tempWorld, tempDir, centercx, centercz, simulate): - yield startLength - len(chunks), startLength, p - - i = 0 - for cx, cz in itertools.product( - xrange(centercx - maxRadius, centercx + maxRadius), - xrange(centercz - maxRadius, centercz + maxRadius)): - if level.containsChunk(cx, cz): - chunks.discard((cx, cz)) - elif ((cx, cz) in chunks - and tempWorld.containsChunk(cx, cz) - and tempWorld.getChunk(cx, cz).TerrainPopulated - ): - self.copyChunkAtPosition(tempWorld, level, cx, cz) - i += 1 - chunks.discard((cx, cz)) - yield startLength - len(chunks), startLength - - if length == len(chunks): - print "No chunks were generated. Aborting." - break - - level.saveInPlace() - - def runServer(self, startingDir): - if isinstance(startingDir, unicode): - startingDir = startingDir.encode(sys.getfilesystemencoding()) - - return self._runServer(startingDir, self.serverJarFile) - - lowMemory = False - - @classmethod - def _runServer(cls, startingDir, jarfile): - info("Starting server %s in %s", jarfile, startingDir) - if cls.lowMemory: - memflags = [] - else: - memflags = ["-Xmx1024M", "-Xms1024M", ] - - proc = subprocess.Popen([cls.javaExe, "-Djava.awt.headless=true"] + memflags + ["-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 proc.poll() is None: - 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 - - if proc.returncode is None: - try: - proc.kill() - except WindowsError: - pass # access denied, process already terminated - - proc.wait() - shutil.rmtree(tempdir) - if ";)" in version: - version = version.replace(";)", "") # Damnit, Jeb! - # Versions like "0.2.1" are alphas, and versions like "1.0.0" without "Beta" are releases - if version[0] == "0": - version = "Alpha " + version - try: - if int(version[0]) > 0: - version = "Release " + version - except ValueError: - pass - - return version - _zeros = {} - def ZeroChunk(height=512): z = _zeros.get(height) if z is None: z = _zeros[height] = _ZeroChunk(height) return z -from level import ChunkBase class _ZeroChunk(ChunkBase): diff --git a/mclevelbase.py b/mclevelbase.py index 5ac1ef4..f19034a 100644 --- a/mclevelbase.py +++ b/mclevelbase.py @@ -78,3 +78,11 @@ else: saveFileDir = os.path.join(minecraftDir, u"saves") + + +if sys.platform == "win32": + appSupportDir = os.path.join(appDataDir, u"pymclevel") +elif sys.platform == "darwin": + appSupportDir = os.path.expanduser(u"~/Library/Application Support/pymclevel/") +else: + appSupportDir = os.path.expanduser(u"~/.pymclevel") diff --git a/minecraft_server.py b/minecraft_server.py new file mode 100644 index 0000000..7ec8eb2 --- /dev/null +++ b/minecraft_server.py @@ -0,0 +1,550 @@ +import itertools +import logging +import os +from os.path import dirname, join, basename +import random +import re +import shutil +import subprocess +import sys +import tempfile +import time +import urllib + +import infiniteworld +from mclevelbase import appSupportDir, exhaust, ChunkNotPresent + +log = logging.getLogger(__name__) + +__author__ = 'Rio' + +# Thank you, Stackoverflow +# http://stackoverflow.com/questions/377017/test-if-executable-exists-in-python +def which(program): + def is_exe(f): + return os.path.exists(f) and os.access(f, os.X_OK) + + fpath, _fname = os.path.split(program) + if fpath: + if is_exe(program): + return program + else: + if sys.platform == "win32": + if "SYSTEMROOT" in os.environ: + root = os.environ["SYSTEMROOT"] + exe_file = os.path.join(root, program) + if is_exe(exe_file): + return exe_file + if "PATH" in os.environ: + 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 + + +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) + + +class ServerJarStorage(object): + defaultCacheDir = os.path.join(appSupportDir, u"ServerJarStorage") + + def __init__(self, cacheDir=None): + if cacheDir is None: + cacheDir = self.defaultCacheDir + + self.cacheDir = cacheDir + + if not os.path.exists(self.cacheDir): + os.makedirs(self.cacheDir) + readme = os.path.join(self.cacheDir, "README.TXT") + if not os.path.exists(readme): + with file(readme, "w") as f: + f.write(""" +About this folder: + +This folder is used by MCEdit and pymclevel to store different versions of the +Minecraft Server to use for terrain generation. It should have one or more +subfolders, one for each version of the server. Each subfolder must hold at +least one file named minecraft_server.jar, and the subfolder's name should +have the server's version plus the names of any installed mods. + +There may already be a subfolder here (for example, "Beta 1.7.3") if you have +used the Chunk Create feature in MCEdit to create chunks using the server. + +Version numbers can be automatically detected. If you place one or more +minecraft_server.jar files in this folder, they will be placed automatically +into well-named subfolders the next time you run MCEdit. If a file's name +begins with "minecraft_server" and ends with ".jar", it will be detected in +this way. +""") + + self.reloadVersions() + + def reloadVersions(self): + cacheDirList = os.listdir(self.cacheDir) + self.versions = list(reversed(sorted([v for v in cacheDirList if os.path.exists(self.jarfileForVersion(v))], key=alphanum_key))) + + if MCServerChunkGenerator.javaExe: + 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 storage initialized." + print u"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, allowDuplicate=False) + + def cacheNewVersion(self, filename, allowDuplicate=True): + """ 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) + + i = 1 + newVersionDir = versionDir + while os.path.exists(newVersionDir): + if not allowDuplicate: + return + + newVersionDir = versionDir + " (" + str(i) + ")" + i += 1 + + os.mkdir(newVersionDir) + + shutil.copy2(filename, os.path.join(newVersionDir, "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").encode(sys.getfilesystemencoding()) + + def checksumForVersion(self, v): + jf = self.jarfileForVersion(v) + with file(jf, "rb") as f: + import hashlib + return hashlib.md5(f.read()).hexdigest() + + broken_versions = ["Beta 1.9 Prerelease {0}".format(i) for i in (1, 2, 3)] + + @property + def latestVersion(self): + if len(self.versions) == 0: + return None + return max((v for v in self.versions if v not in self.broken_versions), key=alphanum_key) + + def getJarfile(self, version=None): + if len(self.versions) == 0: + print "No servers found in cache." + self.downloadCurrentServer() + + version = version or self.latestVersion + if version not in self.versions: + return None + return self.jarfileForVersion(version) + + +class JavaNotFound(RuntimeError): + pass + + +class VersionNotFound(RuntimeError): + pass + + +def readProperties(filename): + if not os.path.exists(filename): + return {} + + with file(filename) as f: + properties = dict((line.split("=", 2) for line in (l.strip() for l in f) if not line.startswith("#"))) + + return properties + + +def saveProperties(filename, properties): + with file(filename, "w") as f: + for k, v in properties.iteritems(): + f.write("{0}={1}\n".format(k, v)) + + +def findJava(): + if sys.platform == "win32": + javaExe = which("java.exe") + if javaExe is None: + KEY_NAME = "HKLM\SOFTWARE\JavaSoft\Java Runtime Environment" + try: + p = subprocess.Popen(["REG", "QUERY", KEY_NAME, "/v", "CurrentVersion"], stdout=subprocess.PIPE, universal_newlines=True) + o, e = p.communicate() + lines = o.split("\n") + for l in lines: + l = l.strip() + if l.startswith("CurrentVersion"): + words = l.split(None, 2) + version = words[-1] + p = subprocess.Popen(["REG", "QUERY", KEY_NAME + "\\" + version, "/v", "JavaHome"], stdout=subprocess.PIPE, universal_newlines=True) + o, e = p.communicate() + lines = o.split("\n") + for l in lines: + l = l.strip() + if l.startswith("JavaHome"): + w = l.split(None, 2) + javaHome = w[-1] + javaExe = os.path.join(javaHome, "bin", "java.exe") + print "RegQuery: java.exe found at ", javaExe + break + + except Exception, e: + print "Error while locating java.exe using the Registry: ", repr(e) + else: + javaExe = which("java") + + return javaExe + + +class MCServerChunkGenerator(object): + """Generates chunks using minecraft_server.jar. Uses a ServerJarStorage 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") + + """ + defaultJarStorage = None + + javaExe = findJava() + jarStorage = None + tempWorldCache = {} + + def __init__(self, version=None, jarfile=None, jarStorage=None): + + self.jarStorage = jarStorage or self.getDefaultJarStorage() + + 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.jarStorage.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.jarStorage.cacheDir)) + self.serverJarFile = jarfile + self.serverVersion = version or self._serverVersion() + + @classmethod + def getDefaultJarStorage(cls): + if cls.defaultJarStorage is None: + cls.defaultJarStorage = ServerJarStorage() + return cls.defaultJarStorage + + @classmethod + def clearWorldCache(cls): + cls.tempWorldCache = {} + + for tempDir in os.listdir(cls.worldCacheDir): + t = os.path.join(cls.worldCacheDir, tempDir) + if os.path.isdir(t): + shutil.rmtree(t) + + def createReadme(self): + readme = os.path.join(self.worldCacheDir, "README.TXT") + + if not os.path.exists(readme): + with file(readme, "w") as f: + f.write(""" + About this folder: + + This folder is used by MCEdit and pymclevel to cache levels during terrain + generation. Feel free to delete it for any reason. + """) + + worldCacheDir = os.path.join(tempfile.gettempdir(), "pymclevel_MCServerChunkGenerator") + + def tempWorldForLevel(self, level): + + # tempDir = tempfile.mkdtemp("mclevel_servergen") + tempDir = os.path.join(self.worldCacheDir, self.jarStorage.checksumForVersion(self.serverVersion), str(level.RandomSeed)) + propsFile = os.path.join(tempDir, "server.properties") + properties = readProperties(propsFile) + + tempWorld = self.tempWorldCache.get((self.serverVersion, level.RandomSeed)) + + if tempWorld is None: + if not os.path.exists(tempDir): + os.makedirs(tempDir) + self.createReadme() + + worldName = "world" + worldName = properties.setdefault("level-name", worldName) + + tempWorldDir = os.path.join(tempDir, worldName) + tempWorld = infiniteworld.MCInfdevOldLevel(tempWorldDir, create=True, random_seed=level.RandomSeed) + del tempWorld.version # for compatibility with older servers. newer ones will set it again without issue. + + self.tempWorldCache[self.serverVersion, level.RandomSeed] = tempWorld + + if level.dimNo == 0: + properties["allow-nether"] = "false" + else: + tempWorld = tempWorld.getDimension(level.dimNo) + + properties["allow-nether"] = "true" + + properties["server-port"] = int(32767 + random.random() * 32700) + saveProperties(propsFile, properties) + + return tempWorld, tempDir + + def generateAtPosition(self, tempWorld, tempDir, cx, cz): + return exhaust(self.generateAtPositionIter(tempWorld, tempDir, cx, cz)) + + def generateAtPositionIter(self, tempWorld, tempDir, cx, cz, simulate=False): + tempWorld.setPlayerSpawnPosition((cx * 16, 64, cz * 16)) + tempWorld.saveInPlace() + tempWorld.unloadRegions() + + startTime = time.time() + proc = self.runServer(tempDir) + while proc.poll() is None: + line = proc.stderr.readline().strip() + log.info(line) + yield line + + if "[INFO] Done" in line: + if simulate: + duration = time.time() - startTime + + simSeconds = max(8, int(duration) + 1) + + for i in range(simSeconds): + # process tile ticks + yield "%2d/%2d: Simulating the world for a little bit..." % (i, simSeconds) + time.sleep(1) + + proc.stdin.write("stop\n") + proc.wait() + break + if "FAILED TO BIND" in line: + proc.kill() + proc.wait() + raise RuntimeError("Server failed to bind to port!") + + stdout, _ = proc.communicate() + + if "Could not reserve enough space" in stdout and not MCServerChunkGenerator.lowMemory: + MCServerChunkGenerator.lowMemory = True + for i in self.generateAtPositionIter(tempWorld, tempDir, cx, cz): + yield i + + (tempWorld.parentWorld or tempWorld).loadLevelDat() # reload version number + + def copyChunkAtPosition(self, tempWorld, level, cx, cz): + if level.containsChunk(cx, cz): + return + try: + tempChunk = tempWorld.getChunk(cx, cz) + except ChunkNotPresent, e: + raise ChunkNotPresent("While generating a world in {0} using server {1} ({2!r})".format(tempWorld, self.serverJarFile, e), sys.exc_traceback) + + if not level.containsChunk(cx, cz): + level.createChunk(cx, cz) + + chunk = level.getChunk(cx, cz) + chunk.root_tag = tempChunk.root_tag + chunk.dirty = True + + chunk.save() + + + def generateChunkInLevel(self, level, cx, cz): + assert isinstance(level, infiniteworld.MCInfdevOldLevel) + + tempWorld, tempDir = self.tempWorldForLevel(level) + self.generateAtPosition(tempWorld, tempDir, cx, cz) + self.copyChunkAtPosition(tempWorld, level, cx, cz) + + minRadius = 5 + maxRadius = 20 + + def createLevel(self, level, box, simulate=False, **kw): + return exhaust(self.createLevelIter(level, box, simulate, **kw)) + + def createLevelIter(self, level, box, simulate=False, **kw): + if isinstance(level, basestring): + filename = level + level = infiniteworld.MCInfdevOldLevel(filename, create=True, **kw) + + assert isinstance(level, infiniteworld.MCInfdevOldLevel) + minRadius = self.minRadius + + genPositions = list(itertools.product( + xrange(box.mincx, box.maxcx, minRadius * 2), + xrange(box.mincz, box.maxcz, minRadius * 2))) + + for i, (cx, cz) in enumerate(genPositions): + log.info("Generating at %s" % ((cx, cz),)) + parentDir = dirname(level.worldDir) + propsFile = join(parentDir, "server.properties") + props = readProperties(join(dirname(self.serverJarFile), "server.properties")) + props["level-name"] = basename(level.worldDir) + props["server-port"] = int(32767 + random.random() * 32700) + saveProperties(propsFile, props) + + for p in self.generateAtPositionIter(level, parentDir, cx, cz, simulate): + yield i, len(genPositions), p + + level.unloadRegions() + + def generateChunksInLevel(self, level, chunks): + return exhaust(self.generateChunksInLevelIter(level, chunks)) + + def generateChunksInLevelIter(self, level, chunks, simulate=False): + assert isinstance(level, infiniteworld.MCInfdevOldLevel) + tempWorld, tempDir = self.tempWorldForLevel(level) + + startLength = len(chunks) + minRadius = self.minRadius + maxRadius = self.maxRadius + chunks = set(chunks) + + while len(chunks): + length = len(chunks) + centercx, centercz = chunks.pop() + chunks.add((centercx, centercz)) + # assume the generator always generates at least an 11x11 chunk square. + centercx += minRadius + centercz += minRadius + + # boxedChunks = [cPos for cPos in chunks if inBox(cPos)] + + print "Generating {0} chunks out of {1} starting from {2}".format("XXX", len(chunks), (centercx, centercz)) + yield startLength - len(chunks), startLength + + # chunks = [c for c in chunks if not inBox(c)] + + for p in self.generateAtPositionIter(tempWorld, tempDir, centercx, centercz, simulate): + yield startLength - len(chunks), startLength, p + + i = 0 + for cx, cz in itertools.product( + xrange(centercx - maxRadius, centercx + maxRadius), + xrange(centercz - maxRadius, centercz + maxRadius)): + if level.containsChunk(cx, cz): + chunks.discard((cx, cz)) + elif ((cx, cz) in chunks + and tempWorld.containsChunk(cx, cz) + and tempWorld.getChunk(cx, cz).TerrainPopulated + ): + self.copyChunkAtPosition(tempWorld, level, cx, cz) + i += 1 + chunks.discard((cx, cz)) + yield startLength - len(chunks), startLength + + if length == len(chunks): + print "No chunks were generated. Aborting." + break + + level.saveInPlace() + + def runServer(self, startingDir): + if isinstance(startingDir, unicode): + startingDir = startingDir.encode(sys.getfilesystemencoding()) + + return self._runServer(startingDir, self.serverJarFile) + + lowMemory = False + + @classmethod + def _runServer(cls, startingDir, jarfile): + log.info("Starting server %s in %s", jarfile, startingDir) + if cls.lowMemory: + memflags = [] + else: + memflags = ["-Xmx1024M", "-Xms1024M", ] + + proc = subprocess.Popen([cls.javaExe, "-Djava.awt.headless=true"] + memflags + ["-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 proc.poll() is None: + 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 + + if proc.returncode is None: + try: + proc.kill() + except WindowsError: + pass # access denied, process already terminated + + proc.wait() + shutil.rmtree(tempdir) + if ";)" in version: + version = version.replace(";)", "") # Damnit, Jeb! + # Versions like "0.2.1" are alphas, and versions like "1.0.0" without "Beta" are releases + if version[0] == "0": + version = "Alpha " + version + try: + if int(version[0]) > 0: + version = "Release " + version + except ValueError: + pass + + return version diff --git a/test/server_test.py b/test/server_test.py index f158a66..71f76d6 100644 --- a/test/server_test.py +++ b/test/server_test.py @@ -1,5 +1,5 @@ import unittest -from infiniteworld import MCServerChunkGenerator +from minecraft_server import MCServerChunkGenerator from templevel import TempLevel from box import BoundingBox