rename ServerJarCache to ServerJarStorage. clean up code, use readProperties and writeProperties to handle server.properties, allow generation in the Nether

This commit is contained in:
David Vierra 2011-08-31 08:32:25 -10:00
parent 05cd068104
commit d7e1e88dc0

View File

@ -37,15 +37,20 @@ Player = 'Player'
__all__ = ["ZeroChunk", "InfdevChunk", "MCInfdevOldLevel", "MCAlphaDimension", "ZipSchematic"] __all__ = ["ZeroChunk", "InfdevChunk", "MCInfdevOldLevel", "MCAlphaDimension", "ZipSchematic"]
def exhaust(_iter):
i = 0
for i in _iter:
pass
return i
import re import re
convert = lambda text: int(text) if text.isdigit() else text convert = lambda text: int(text) if text.isdigit() else text
alphanum_key = lambda key: [ convert(c) for c in re.split('([0-9]+)', key) ] alphanum_key = lambda key: [ convert(c) for c in re.split('([0-9]+)', key) ]
def sort_nicely(l): def sort_nicely(l):
""" Sort the given list in the way that humans expect. """ Sort the given list in the way that humans expect.
""" """
l.sort(key=alphanum_key) l.sort(key=alphanum_key)
# Thank you, Stackoverflow # Thank you, Stackoverflow
# http://stackoverflow.com/questions/377017/test-if-executable-exists-in-python # http://stackoverflow.com/questions/377017/test-if-executable-exists-in-python
@ -73,8 +78,14 @@ else:
appSupportDir = os.path.expanduser("~/.pymclevel") appSupportDir = os.path.expanduser("~/.pymclevel")
class ServerJarStorage(object): class ServerJarStorage(object):
cacheDir = os.path.join(appSupportDir, "ServerJarStorage") defaultCacheDir = os.path.join(appSupportDir, "ServerJarStorage")
def __init__(self):
def __init__(self, cacheDir=None):
if cacheDir is None:
cacheDir = self.defaultCacheDir
self.cacheDir = cacheDir
if not os.path.exists(self.cacheDir): if not os.path.exists(self.cacheDir):
os.makedirs(self.cacheDir) os.makedirs(self.cacheDir)
readme = os.path.join(self.cacheDir, "README.TXT") readme = os.path.join(self.cacheDir, "README.TXT")
@ -85,9 +96,9 @@ About this folder:
This folder is used by MCEdit and pymclevel to store different versions of the 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 Minecraft Server to use for terrain generation. It should have one or more
subfolders, one for each version of the server. Each subfolder must contain at 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 least one file named minecraft_server.jar, and the subfolder's name should
contain the server's version and the names of any installed mods. 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 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. used the Chunk Create feature in MCEdit to create chunks using the server.
@ -99,7 +110,9 @@ begins with "minecraft_server" and ends with ".jar", it will be detected in
this way. this way.
""") """)
self.reloadVersions()
def reloadVersions(self):
cacheDirList = os.listdir(self.cacheDir) 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))) self.versions = list(reversed(sorted([v for v in cacheDirList if os.path.exists(self.jarfileForVersion(v))], key=alphanum_key)))
@ -111,7 +124,7 @@ this way.
os.remove(p) os.remove(p)
print "Minecraft_Server.jar cache initialized." print "Minecraft_Server.jar storage initialized."
print "Each server is stored in a subdirectory of {0} named with the server's version number".format(self.cacheDir) print "Each server is stored in a subdirectory of {0} named with the server's version number".format(self.cacheDir)
@ -135,8 +148,14 @@ this way.
version = MCServerChunkGenerator._serverVersionFromJarFile(filename) version = MCServerChunkGenerator._serverVersionFromJarFile(filename)
print "Found version ", version print "Found version ", version
versionDir = os.path.join(self.cacheDir, version) versionDir = os.path.join(self.cacheDir, version)
if not os.path.exists(versionDir):
os.mkdir(versionDir) i = 1
versionDir2 = versionDir
while os.path.exists(versionDir2):
versionDir2 = versionDir + str(i)
i += 1
os.mkdir(versionDir2)
shutil.copy2(filename, os.path.join(versionDir, "minecraft_server.jar")) shutil.copy2(filename, os.path.join(versionDir, "minecraft_server.jar"))
@ -168,6 +187,17 @@ this way.
class JavaNotFound(RuntimeError): pass class JavaNotFound(RuntimeError): pass
class VersionNotFound(RuntimeError): pass class VersionNotFound(RuntimeError): pass
def readProperties(filename):
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))
class MCServerChunkGenerator(object): class MCServerChunkGenerator(object):
"""Generates chunks using minecraft_server.jar. Uses a ServerJarStorage to """Generates chunks using minecraft_server.jar. Uses a ServerJarStorage to
store different versions of minecraft_server.jar in an application support store different versions of minecraft_server.jar in an application support
@ -192,25 +222,32 @@ class MCServerChunkGenerator(object):
gen = MCServerChunkGenerator("Beta 1.6.5") gen = MCServerChunkGenerator("Beta 1.6.5")
""" """
defaultJarStorage = None
if sys.platform == "win32": if sys.platform == "win32":
javaExe = which("java.exe") javaExe = which("java.exe")
else: else:
javaExe = which("java") javaExe = which("java")
jarcache = ServerJarStorage() jarStorage = None
tempWorldCache = {} tempWorldCache = {}
@classmethod @classmethod
def reloadJarCache(cls): def reloadJarCache(cls):
cls.jarcache = ServerJarStorage() cls.defaultJarStorage.reloadVersions()
def __init__(self, version=None, jarfile=None, jarStorage=None):
if self.__class__.defaultJarStorage is None:
self.__class__.defaultJarStorage = ServerJarStorage()
self.jarStorage = jarStorage or self.defaultJarStorage
def __init__(self, version=None, jarfile=None):
if self.javaExe is None: 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.)" 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: if jarfile is None:
jarfile = self.jarcache.getJarfile(version) jarfile = self.jarStorage.getJarfile(version)
if jarfile is None: 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) 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.serverJarFile = jarfile
self.serverVersion = self._serverVersion() self.serverVersion = self._serverVersion()
@ -223,64 +260,58 @@ class MCServerChunkGenerator(object):
if os.path.isdir(t): if os.path.isdir(t):
shutil.rmtree(t) shutil.rmtree(t)
def waitForServer(self, proc): def createReadme(self):
""" 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!"
worldCacheDir = os.path.join(tempfile.gettempdir(), "pymclevel_MCServerChunkGenerator")
def tempWorldForLevel(self, level):
if level.RandomSeed in self.tempWorldCache:
return self.tempWorldCache[level.RandomSeed]
#tempDir = tempfile.mkdtemp("mclevel_servergen")
tempDir = os.path.join(self.worldCacheDir, self.jarcache.checksumForVersion(self.serverVersion), str(level.RandomSeed))
readme = os.path.join(self.worldCacheDir, "README.TXT") readme = os.path.join(self.worldCacheDir, "README.TXT")
if not os.path.exists(readme): if not os.path.exists(readme):
with file(readme, "w") as f: with file(readme, "w") as f:
f.write(""" f.write("""
About this folder: 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.
""")
This folder is used by MCEdit and pymclevel to cache levels during terrain worldCacheDir = os.path.join(tempfile.gettempdir(), "pymclevel_MCServerChunkGenerator")
generation. Feel free to delete it for any reason.
""")
if not os.path.exists(tempDir): def tempWorldForLevel(self, level):
os.makedirs(tempDir)
tempWorldDir = os.path.join(tempDir, "world")
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[level.RandomSeed] = (tempWorld, tempDir)
#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") propsFile = os.path.join(tempDir, "server.properties")
if level.dimNo == 0: if os.path.exists(propsFile):
with file(propsFile, "w") as f: properties = readProperties(propsFile)
f.write("allow-nether=false\n")
else: else:
with file(propsFile, "w") as f: properties = {}
f.write("allow-nether=true\n")
if level.RandomSeed in self.tempWorldCache:
tempWorld = self.tempWorldCache[level.RandomSeed]
else:
if not os.path.exists(tempDir):
os.makedirs(tempDir)
self.createReadme()
worldName = "world"
worldName = properties.setdefault("world-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[level.RandomSeed] = tempWorld
if level.dimNo == 0:
properties["allow-nether"] = "false"
else:
tempWorld = tempWorld.getDimension(level.dimNo)
properties["allow-nether"] = "true"
saveProperties(propsFile, properties)
return (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): def generateAtPosition(self, tempWorld, tempDir, cx, cz):
tempWorld.setPlayerSpawnPosition((cx * 16, 64, cz * 16)) tempWorld.setPlayerSpawnPosition((cx * 16, 64, cz * 16))
tempWorld.saveInPlace() tempWorld.saveInPlace()
@ -289,7 +320,7 @@ generation. Feel free to delete it for any reason.
proc = self.runServer(tempDir) proc = self.runServer(tempDir)
self.waitForServer(proc) self.waitForServer(proc)
tempWorld.loadLevelDat() #reload version number (tempWorld.parentWorld or tempWorld).loadLevelDat() #reload version number
def copyChunkAtPosition(self, tempWorld, level, cx, cz): def copyChunkAtPosition(self, tempWorld, level, cx, cz):
if level.containsChunk(cx, cz): return if level.containsChunk(cx, cz): return
@ -315,9 +346,15 @@ generation. Feel free to delete it for any reason.
tempChunk.compress() tempChunk.compress()
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 generateChunksInLevel(self, level, chunks): def generateChunksInLevel(self, level, chunks):
for i in self.generateChunksInLevelIter(level, chunks): return exhaust(self.generateChunksInLevelIter(level, chunks))
yield i
def generateChunksInLevelIter(self, level, chunks): def generateChunksInLevelIter(self, level, chunks):
assert isinstance(level, MCInfdevOldLevel) assert isinstance(level, MCInfdevOldLevel)
@ -353,13 +390,27 @@ generation. Feel free to delete it for any reason.
level.saveInPlace() level.saveInPlace()
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 runServer(self, startingDir): def runServer(self, startingDir):
return self._runServer(startingDir, self.serverJarFile) return self._runServer(startingDir, self.serverJarFile)
@classmethod @classmethod
def _runServer(cls, startingDir, jarfile): def _runServer(cls, startingDir, jarfile):
print "Starting server {0} in {1}".format(jarfile, startingDir) print "Starting server {0} in {1}".format(jarfile, startingDir)
proc = subprocess.Popen([cls.javaExe, "-jar", jarfile], proc = subprocess.Popen([cls.javaExe, "-Xmx1024M", "-Xms1024M", "-jar", jarfile],
executable=cls.javaExe, executable=cls.javaExe,
cwd=startingDir, cwd=startingDir,
stdin=subprocess.PIPE, stdin=subprocess.PIPE,
@ -388,8 +439,12 @@ generation. Feel free to delete it for any reason.
version = line.split("Starting minecraft server version")[1].strip() version = line.split("Starting minecraft server version")[1].strip()
break break
if proc.returncode is None:
try:
proc.kill()
except WindowsError:
pass #access denied, process already terminated
proc.kill()
proc.wait() proc.wait()
shutil.rmtree(tempdir) shutil.rmtree(tempdir)
@ -1252,7 +1307,7 @@ class MCInfdevOldLevel(EntityLevel):
def __str__(self): def __str__(self):
return "MCInfdevOldLevel(" + os.path.split(self.worldDir)[1] + ")" return "MCInfdevOldLevel(\"" + os.path.split(self.worldDir)[1] + "\")"
def TagProperty(tagName, tagType, defaultValueFunc=lambda self:None): def TagProperty(tagName, tagType, defaultValueFunc=lambda self:None):
def getter(self): def getter(self):
@ -1268,7 +1323,7 @@ class MCInfdevOldLevel(EntityLevel):
SizeOnDisk = TagProperty('SizeOnDisk', TAG_Long) SizeOnDisk = TagProperty('SizeOnDisk', TAG_Long)
RandomSeed = TagProperty('RandomSeed', TAG_Long) RandomSeed = TagProperty('RandomSeed', TAG_Long)
Time = TagProperty('Time', TAG_Long); """ Age of the world in ticks. 20 ticks per second; 24000 ticks per day.""" Time = TagProperty('Time', TAG_Long); """ Age of the world in ticks. 20 ticks per second; 24000 ticks per day."""
LastPlayed = TagProperty('LastPlayed', TAG_Long, lambda self:long(time.time()*1000)) LastPlayed = TagProperty('LastPlayed', TAG_Long, lambda self:long(time.time() * 1000))
LevelName = TagProperty('LevelName', TAG_String, lambda self:self.displayName) LevelName = TagProperty('LevelName', TAG_String, lambda self:self.displayName)
@ -1300,7 +1355,7 @@ class MCInfdevOldLevel(EntityLevel):
root_tag[Data][SpawnZ] = TAG_Int(0) root_tag[Data][SpawnZ] = TAG_Int(0)
if last_played is None: if last_played is None:
last_played = long(time.time()*1000) last_played = long(time.time() * 1000)
if random_seed is None: if random_seed is None:
random_seed = long(random.random() * 0xffffffffffffffffL) - 0x8000000000000000L random_seed = long(random.random() * 0xffffffffffffffffL) - 0x8000000000000000L
@ -1442,18 +1497,23 @@ class MCInfdevOldLevel(EntityLevel):
try: try:
dimNo = int(dirname[3:]); dimNo = int(dirname[3:]);
info("Found dimension {0}".format(dirname)) info("Found dimension {0}".format(dirname))
dim = MCAlphaDimension(filename=os.path.join(self.worldDir, dirname)); dim = MCAlphaDimension(self, dimNo);
dim.parentWorld = self;
dim.dimNo = dimNo
dim.root_tag = self.root_tag;
dim.filename = self.filename
dim.playersDir = self.playersDir;
dim.players = self.players
self.dimensions[dimNo] = dim; self.dimensions[dimNo] = dim;
except Exception, e: except Exception, e:
error(u"Error loading dimension {0}: {1}".format(dirname, e)) error(u"Error loading dimension {0}: {1}".format(dirname, e))
def getDimension(self, dimNo):
if self.dimNo != 0:
return self.parentWorld.getDimension(dimNo)
if dimNo == 0:
return self
if dimNo in self.dimensions: return self.dimensions[dimNo]
dim = MCAlphaDimension(self, dimNo, create=True)
self.dimensions[dimNo] = dim
return dim
def getRegionForChunk(self, cx, cz): def getRegionForChunk(self, cx, cz):
rx = cx >> 5 rx = cx >> 5
@ -2713,12 +2773,31 @@ class MCInfdevOldLevel(EntityLevel):
return array(yp); return array(yp);
class MCAlphaDimension (MCInfdevOldLevel): class MCAlphaDimension (MCInfdevOldLevel):
def __init__(self, parentWorld, dimNo):
filename = os.path.join(parentWorld.worldDir, "DIM" + str(int(dimNo)))
MCInfdevOldLevel.__init__(self, filename)
self.dimNo = dimNo
self.parentWorld = parentWorld;
self.filename = parentWorld.filename
self.playersDir = parentWorld.playersDir;
self.players = parentWorld.players
@property
def root_tag(self): return self.parentWorld.root_tag;
def __str__(self):
return "MCAlphaDimension({0}, {1})".format(self.parentWorld, self.dimNo)
def loadLevelDat(self, create=False, random_seed=None, last_played=None): def loadLevelDat(self, create=False, random_seed=None, last_played=None):
pass; pass;
def preloadDimensions(self): def preloadDimensions(self):
pass pass
def create(self, *args, **kw):
if not os.path.exists(self.worldDir):
os.mkdir(self.worldDir)
dimensionNames = { -1: "Nether" }; dimensionNames = { -1: "Nether", 3: "Aether"};
@property @property
def displayName(self): def displayName(self):
return u"{0} ({1})".format(self.parentWorld.displayName, self.dimensionNames[self.dimNo]) return u"{0} ({1})".format(self.parentWorld.displayName, self.dimensionNames[self.dimNo])