more work on server-based generation: use hashes instead of version numbers, clear the world cache via listdir, put readmes into the two folders, fix compatibility with older servers

This commit is contained in:
David Vierra 2011-08-23 08:27:53 -10:00
parent 675f814b78
commit 6f035c4aaa

View File

@ -80,9 +80,31 @@ class ServerJarCache(object):
self.cacheDir = cacheDir 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")
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 contain at
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.
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.
""")
cacheDirList = os.listdir(self.cacheDir) cacheDirList = os.listdir(self.cacheDir)
self.versions = [v for v in cacheDirList if os.path.exists(self.jarfileForVersion(v))] self.versions = list(reversed(sorted([v for v in cacheDirList if os.path.exists(self.jarfileForVersion(v))], key=alphanum_key)))
for f in cacheDirList: for f in cacheDirList:
p = os.path.join(self.cacheDir, f) p = os.path.join(self.cacheDir, f)
@ -126,6 +148,11 @@ class ServerJarCache(object):
def jarfileForVersion(self, v): def jarfileForVersion(self, v):
return os.path.join(self.cacheDir, v, "minecraft_server.jar") return os.path.join(self.cacheDir, v, "minecraft_server.jar")
def checksumForVersion(self, v):
jf = self.jarfileForVersion(v)
with file(jf, "rb") as f:
import md5
return (md5.md5(f.read()).hexdigest())
@property @property
def latestVersion(self): def latestVersion(self):
@ -174,11 +201,13 @@ class MCServerChunkGenerator(object):
else: else:
javaExe = which("java") javaExe = which("java")
jarcache = None jarcache = ServerJarCache()
tempWorldCache = {}
@classmethod
def reloadJarCache(cls):
cls.jarcache = ServerJarCache()
def __init__(self, version=None, jarfile=None): def __init__(self, version=None, jarfile=None):
if self.__class__.jarcache is None:
self.__class__.jarcache = ServerJarCache()
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:
@ -187,14 +216,13 @@ class MCServerChunkGenerator(object):
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.jarcache.cacheDir)
self.serverJarFile = jarfile self.serverJarFile = jarfile
self.serverVersion = self._serverVersion() self.serverVersion = self._serverVersion()
self.tempWorldCache = {}
def clearWorldCache(self): @classmethod
tempDirs = [tempDir for tempDir, _tempWorld in self.tempWorldCache.itervalues()] def clearWorldCache(cls):
#self.tempWorldCache = None cls.tempWorldCache = {}
for tempDir in tempDirs: for tempDir in os.listdir(cls.worldCacheDir):
shutil.rmtree(tempDir) shutil.rmtree(os.path.join(cls.worldCacheDir, tempDir))
def waitForServer(self, proc): def waitForServer(self, proc):
""" wait for the server to finish starting up, then stop it. """ """ wait for the server to finish starting up, then stop it. """
@ -209,17 +237,32 @@ class MCServerChunkGenerator(object):
proc.wait() proc.wait()
raise RuntimeError, "Server Died!" raise RuntimeError, "Server Died!"
worldCacheDir = os.path.join(tempfile.gettempdir(), "pymclevel_MCServerChunkGenerator")
def tempWorldForLevel(self, level): def tempWorldForLevel(self, level):
if level.RandomSeed in self.tempWorldCache: if level.RandomSeed in self.tempWorldCache:
return self.tempWorldCache[level.RandomSeed] return self.tempWorldCache[level.RandomSeed]
#tempDir = tempfile.mkdtemp("mclevel_servergen") #tempDir = tempfile.mkdtemp("mclevel_servergen")
tempDir = os.path.join(tempfile.gettempdir(), "pymclevel_MCServerChunkGenerator", self.serverVersion, str(level.RandomSeed)) tempDir = os.path.join(self.worldCacheDir, self.jarcache.checksumForVersion(self.serverVersion), str(level.RandomSeed))
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.
""")
if not os.path.exists(tempDir): if not os.path.exists(tempDir):
os.makedirs(tempDir) os.makedirs(tempDir)
tempWorldDir = os.path.join(tempDir, "world") tempWorldDir = os.path.join(tempDir, "world")
tempWorld = MCInfdevOldLevel(tempWorldDir, create=True, random_seed=level.RandomSeed) 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) self.tempWorldCache[level.RandomSeed] = (tempWorld, tempDir)
return (tempWorld, tempDir) return (tempWorld, tempDir)
@ -233,12 +276,17 @@ class MCServerChunkGenerator(object):
tempWorld.setPlayerSpawnPosition((cx * 16, 64, cz * 16)) tempWorld.setPlayerSpawnPosition((cx * 16, 64, cz * 16))
tempWorld.saveInPlace() tempWorld.saveInPlace()
tempWorld.unloadRegions() tempWorld.unloadRegions()
proc = self.runServer(tempDir) proc = self.runServer(tempDir)
self.waitForServer(proc) self.waitForServer(proc)
tempWorld.loadLevelDat() #reload version number
def copyChunkAtPosition(self, tempWorld, level, cx, cz): def copyChunkAtPosition(self, tempWorld, level, cx, cz):
tempChunk = tempWorld.getChunk(cx, cz) 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
tempChunk.decompress() tempChunk.decompress()
tempChunk.unpackChunkData() tempChunk.unpackChunkData()
root_tag = tempChunk.root_tag root_tag = tempChunk.root_tag
@ -257,23 +305,40 @@ class MCServerChunkGenerator(object):
def generateChunksInLevel(self, level, chunks): def generateChunksInLevel(self, level, chunks):
for i in self.generateChunksInLevelIter(level, chunks):
yield i
def generateChunksInLevelIter(self, level, chunks):
assert isinstance(level, MCInfdevOldLevel) assert isinstance(level, MCInfdevOldLevel)
tempWorld, tempDir = self.tempWorldForLevel(level) tempWorld, tempDir = self.tempWorldForLevel(level)
startRegionRadius = 7 #more recent servers use 12
def inBox(cPos): def inBox(cPos):
x, z = cPos x, z = cPos
return x > centercx - 12 and x < centercx + 12 and z > centercz - 12 and z < centercz + 12 return (x > centercx - startRegionRadius
and x < centercx + startRegionRadius
and z > centercz - startRegionRadius
and z < centercz + startRegionRadius)
startLength = len(chunks)
while len(chunks): while len(chunks):
centercx, centercz = chunks[0] centercx, centercz = chunks[0]
boxedChunks = [cPos for cPos in chunks if inBox(cPos)] 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)) print "Generating {0} chunks out of {1} starting from {2}".format(len(boxedChunks), len(chunks), (centercx, centercz))
yield startLength - len(chunks), startLength
chunks = [c for c in chunks if not inBox(c)] chunks = [c for c in chunks if not inBox(c)]
self.generateAtPosition(tempWorld, tempDir, centercx, centercz) self.generateAtPosition(tempWorld, tempDir, centercx, centercz)
for cx, cz in boxedChunks: for cx, cz in boxedChunks:
self.copyChunkAtPosition(tempWorld, level, cx, cz) try:
self.copyChunkAtPosition(tempWorld, level, cx, cz)
except ChunkNotPresent:
print "Failed to copy chunk", (cx, cz), "at a delta of ", (centercx - cx, centercz - cz), "from the last generation center."
raise
level.saveInPlace() level.saveInPlace()
@ -305,7 +370,7 @@ class MCServerChunkGenerator(object):
#out, err = proc.communicate() #out, err = proc.communicate()
#for line in err.split("\n"): #for line in err.split("\n"):
while True: while proc.poll() is None:
line = proc.stderr.readline() line = proc.stderr.readline()
if "Preparing start region" in line: break if "Preparing start region" in line: break
if "Starting minecraft server version" in line: if "Starting minecraft server version" in line:
@ -1337,7 +1402,7 @@ class MCInfdevOldLevel(EntityLevel):
self.preloadDimensions(); self.preloadDimensions();
#self.preloadChunkPositions(); #self.preloadChunkPositions();
def loadLevelDat(self, create, random_seed, last_played): def loadLevelDat(self, create=False, random_seed=None, last_played=None):
if create: if create:
self.create(self.filename, random_seed, last_played); self.create(self.filename, random_seed, last_played);
@ -1528,6 +1593,17 @@ class MCInfdevOldLevel(EntityLevel):
else: else:
return None return None
@version.setter
@decompress_first
def version(self, val):
if 'version' in self.root_tag['Data']:
self.root_tag['Data']['version'].value = val
@version.deleter
@decompress_first
def version(self):
self.root_tag['Data'].pop('version')
def _loadChunk(self, chunk): def _loadChunk(self, chunk):
""" load the chunk data from disk, and set the chunk's compressedTag """ load the chunk data from disk, and set the chunk's compressedTag
@ -1792,7 +1868,7 @@ class MCInfdevOldLevel(EntityLevel):
the chunk is done later, accesses to chunk attributes may the chunk is done later, accesses to chunk attributes may
raise ChunkMalformed""" raise ChunkMalformed"""
if not self.containsChunk(cx, cz): if not self.containsChunk(cx, cz) :
raise ChunkNotPresent, (cx, cz); raise ChunkNotPresent, (cx, cz);
if not (cx, cz) in self._loadedChunks: if not (cx, cz) in self._loadedChunks:
@ -2626,7 +2702,7 @@ class MCInfdevOldLevel(EntityLevel):
return array(yp); return array(yp);
class MCAlphaDimension (MCInfdevOldLevel): class MCAlphaDimension (MCInfdevOldLevel):
def loadLevelDat(self, create, random_seed, last_played): def loadLevelDat(self, create=False, random_seed=None, last_played=None):
pass; pass;
def preloadDimensions(self): def preloadDimensions(self):
pass pass
@ -2738,7 +2814,7 @@ class ZipSchematic (MCInfdevOldLevel):
def preloadDimensions(self): def preloadDimensions(self):
pass pass
def loadLevelDat(self, create, random_seed, last_played): def loadLevelDat(self, create=False, random_seed=None, last_played=None):
if create: if create:
raise NotImplementedError, "Cannot save zipfiles yet!" raise NotImplementedError, "Cannot save zipfiles yet!"