Refactor: Move MCServerChunkGenerator and related classes and functions to a new file minecraft_server.py
This commit is contained in:
parent
0b55db744c
commit
670bb46d8a
564
infiniteworld.py
564
infiniteworld.py
@ -4,29 +4,26 @@ Created on Jul 22, 2011
|
|||||||
@author: Rio
|
@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 contextlib import closing
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from entity import Entity, TileEntity
|
|
||||||
from faces import FaceXDecreasing, FaceXIncreasing, FaceZDecreasing, FaceZIncreasing
|
|
||||||
import itertools
|
import itertools
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
from materials import alphaMaterials, namedMaterials
|
|
||||||
from math import floor
|
from math import floor
|
||||||
from mclevelbase import appDataDir, ChunkMalformed, ChunkNotPresent, exhaust, PlayerNotFound
|
|
||||||
import nbt
|
|
||||||
from numpy import array, clip, maximum, zeros
|
|
||||||
import os
|
import os
|
||||||
from os.path import join, dirname, basename
|
|
||||||
import random
|
import random
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
import zlib
|
import zlib
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
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
|
from regionfile import MCRegionFile
|
||||||
|
|
||||||
log = getLogger(__name__)
|
log = getLogger(__name__)
|
||||||
@ -34,565 +31,20 @@ warn, error, info, debug = log.warn, log.error, log.info, log.debug
|
|||||||
|
|
||||||
import blockrotation
|
import blockrotation
|
||||||
from box import BoundingBox
|
from box import BoundingBox
|
||||||
from level import LightedChunk, EntityLevel, computeChunkHeightMap, MCLevel
|
from level import LightedChunk, EntityLevel, computeChunkHeightMap, MCLevel, ChunkBase
|
||||||
|
|
||||||
# infinite
|
|
||||||
|
|
||||||
DIM_NETHER = -1
|
DIM_NETHER = -1
|
||||||
DIM_END = 1
|
DIM_END = 1
|
||||||
|
|
||||||
__all__ = ["ZeroChunk", "AnvilChunk", "ChunkedLevelMixin", "MCInfdevOldLevel", "MCAlphaDimension", "ZipSchematic"]
|
__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 = {}
|
_zeros = {}
|
||||||
|
|
||||||
|
|
||||||
def ZeroChunk(height=512):
|
def ZeroChunk(height=512):
|
||||||
z = _zeros.get(height)
|
z = _zeros.get(height)
|
||||||
if z is None:
|
if z is None:
|
||||||
z = _zeros[height] = _ZeroChunk(height)
|
z = _zeros[height] = _ZeroChunk(height)
|
||||||
return z
|
return z
|
||||||
|
|
||||||
from level import ChunkBase
|
|
||||||
|
|
||||||
|
|
||||||
class _ZeroChunk(ChunkBase):
|
class _ZeroChunk(ChunkBase):
|
||||||
|
@ -78,3 +78,11 @@ else:
|
|||||||
|
|
||||||
|
|
||||||
saveFileDir = os.path.join(minecraftDir, u"saves")
|
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")
|
||||||
|
550
minecraft_server.py
Normal file
550
minecraft_server.py
Normal file
@ -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
|
@ -1,5 +1,5 @@
|
|||||||
import unittest
|
import unittest
|
||||||
from infiniteworld import MCServerChunkGenerator
|
from minecraft_server import MCServerChunkGenerator
|
||||||
from templevel import TempLevel
|
from templevel import TempLevel
|
||||||
from box import BoundingBox
|
from box import BoundingBox
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user