The spawn point is automatically added to the gmap as a marker. Adding other markers (signs, mob spawners, etc) should be fairly easy. Note: the math that converts from in-game block coordinates to pixel coordinates is iffy. it requires a careful codereview
243 lines
8.3 KiB
Python
243 lines
8.3 KiB
Python
# This file is part of the Minecraft Overviewer.
|
|
#
|
|
# Minecraft Overviewer is free software: you can redistribute it and/or
|
|
# modify it under the terms of the GNU General Public License as published
|
|
# by the Free Software Foundation, either version 3 of the License, or (at
|
|
# your option) any later version.
|
|
#
|
|
# Minecraft Overviewer is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
|
# Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License along
|
|
# with the Overviewer. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
import functools
|
|
import os
|
|
import os.path
|
|
import multiprocessing
|
|
import numpy
|
|
|
|
from PIL import Image
|
|
|
|
import chunk
|
|
import nbt
|
|
|
|
"""
|
|
This module has routines related to generating all the chunks for a world
|
|
and for extracting information about available worlds
|
|
|
|
"""
|
|
|
|
base36decode = functools.partial(int, base=36)
|
|
|
|
|
|
def _convert_coords(chunks):
|
|
"""Takes the list of (chunkx, chunky, chunkfile) where chunkx and chunky
|
|
are in the chunk coordinate system, and figures out the row and column in
|
|
the image each one should be.
|
|
|
|
returns mincol, maxcol, minrow, maxrow, chunks_translated
|
|
chunks_translated is a list of (col, row, filename)
|
|
"""
|
|
chunks_translated = []
|
|
# columns are determined by the sum of the chunk coords, rows are the
|
|
# difference
|
|
item = chunks[0]
|
|
mincol = maxcol = item[0] + item[1]
|
|
minrow = maxrow = item[1] - item[0]
|
|
for c in chunks:
|
|
col = c[0] + c[1]
|
|
mincol = min(mincol, col)
|
|
maxcol = max(maxcol, col)
|
|
row = c[1] - c[0]
|
|
minrow = min(minrow, row)
|
|
maxrow = max(maxrow, row)
|
|
chunks_translated.append((col, row, c[2]))
|
|
|
|
return mincol, maxcol, minrow, maxrow, chunks_translated
|
|
|
|
|
|
def base36encode(number, alphabet='0123456789abcdefghijklmnopqrstuvwxyz'):
|
|
'''
|
|
Convert an integer to a base36 string.
|
|
'''
|
|
if not isinstance(number, (int, long)):
|
|
raise TypeError('number must be an integer')
|
|
|
|
newn = abs(number)
|
|
|
|
# Special case for zero
|
|
if number == 0:
|
|
return '0'
|
|
|
|
base36 = ''
|
|
while newn != 0:
|
|
newn, i = divmod(newn, len(alphabet))
|
|
base36 = alphabet[i] + base36
|
|
|
|
if number < 0:
|
|
return "-" + base36
|
|
return base36
|
|
|
|
class WorldRenderer(object):
|
|
"""Renders a world's worth of chunks.
|
|
worlddir is the path to the minecraft world
|
|
cachedir is the path to a directory that should hold the resulting images.
|
|
It may be the same as worlddir (which used to be the default)"""
|
|
def __init__(self, worlddir, cachedir):
|
|
self.worlddir = worlddir
|
|
self.caves = False
|
|
self.cachedir = cachedir
|
|
|
|
# stores Points Of Interest to be mapped with markers
|
|
# a list of dictionaries, see below for an example
|
|
self.POI = []
|
|
|
|
def findTrueSpawn(self):
|
|
"""Adds the true spawn location to self.POI. The spawn Y coordinate
|
|
is almost always the default of 64. Find the first air block above
|
|
that point for the true spawn location"""
|
|
|
|
## read spawn info from level.dat
|
|
data = nbt.load(os.path.join(self.worlddir, "level.dat"))[1]
|
|
spawnX = data['Data']['SpawnX']
|
|
spawnY = data['Data']['SpawnY']
|
|
spawnZ = data['Data']['SpawnZ']
|
|
|
|
## The chunk that holds the spawn location
|
|
chunkX = spawnX/16
|
|
chunkY = spawnZ/16
|
|
|
|
## The filename of this chunk
|
|
chunkFile = "%s/%s/c.%s.%s.dat" % (base36encode(chunkX % 64),
|
|
base36encode(chunkY % 64),
|
|
base36encode(chunkX),
|
|
base36encode(chunkY))
|
|
|
|
|
|
data=nbt.load(os.path.join(self.worlddir, chunkFile))[1]
|
|
level = data['Level']
|
|
blockArray = numpy.frombuffer(level['Blocks'], dtype=numpy.uint8).reshape((16,16,128))
|
|
|
|
## The block for spawn *within* the chunk
|
|
inChunkX = spawnX - (chunkX*16)
|
|
inChunkZ = spawnZ - (chunkY*16)
|
|
|
|
## find the first air block
|
|
while (blockArray[inChunkX, inChunkZ, spawnY] != 0):
|
|
spawnY += 1
|
|
|
|
|
|
self.POI.append( dict(x=spawnX, y=spawnY, z=spawnZ, msg="Spawn"))
|
|
|
|
def go(self, procs):
|
|
"""Starts the render. This returns when it is finished"""
|
|
|
|
print "Scanning chunks"
|
|
raw_chunks = self._find_chunkfiles()
|
|
|
|
# Translate chunks to our diagonal coordinate system
|
|
mincol, maxcol, minrow, maxrow, chunks = _convert_coords(raw_chunks)
|
|
|
|
self.chunkmap = self._render_chunks_async(chunks, procs)
|
|
|
|
self.mincol = mincol
|
|
self.maxcol = maxcol
|
|
self.minrow = minrow
|
|
self.maxrow = maxrow
|
|
|
|
self.findTrueSpawn()
|
|
|
|
def _find_chunkfiles(self):
|
|
"""Returns a list of all the chunk file locations, and the file they
|
|
correspond to.
|
|
|
|
Returns a list of (chunkx, chunky, filename) where chunkx and chunky are
|
|
given in chunk coordinates. Use convert_coords() to turn the resulting list
|
|
into an oblique coordinate system"""
|
|
all_chunks = []
|
|
for dirpath, dirnames, filenames in os.walk(self.worlddir):
|
|
if not dirnames and filenames:
|
|
for f in filenames:
|
|
if f.startswith("c.") and f.endswith(".dat"):
|
|
p = f.split(".")
|
|
all_chunks.append((base36decode(p[1]), base36decode(p[2]),
|
|
os.path.join(dirpath, f)))
|
|
return all_chunks
|
|
|
|
def _render_chunks_async(self, chunks, processes):
|
|
"""Starts up a process pool and renders all the chunks asynchronously.
|
|
|
|
chunks is a list of (col, row, chunkfile)
|
|
|
|
Returns a dictionary mapping (col, row) to the file where that
|
|
chunk is rendered as an image
|
|
"""
|
|
results = {}
|
|
if processes == 1:
|
|
# Skip the multiprocessing stuff
|
|
print "Rendering chunks synchronously since you requested 1 process"
|
|
for i, (col, row, chunkfile) in enumerate(chunks):
|
|
result = chunk.render_and_save(chunkfile, self.cachedir, cave=self.caves)
|
|
results[(col, row)] = result
|
|
if i > 0:
|
|
if 1000 % i == 0 or i % 1000 == 0:
|
|
print "{0}/{1} chunks rendered".format(i, len(chunks))
|
|
else:
|
|
print "Rendering chunks in {0} processes".format(processes)
|
|
pool = multiprocessing.Pool(processes=processes)
|
|
asyncresults = []
|
|
for col, row, chunkfile in chunks:
|
|
result = pool.apply_async(chunk.render_and_save,
|
|
args=(chunkfile,self.cachedir),
|
|
kwds=dict(cave=self.caves))
|
|
asyncresults.append((col, row, result))
|
|
|
|
pool.close()
|
|
|
|
for i, (col, row, result) in enumerate(asyncresults):
|
|
results[(col, row)] = result.get()
|
|
if i > 0:
|
|
if 1000 % i == 0 or i % 1000 == 0:
|
|
print "{0}/{1} chunks rendered".format(i, len(chunks))
|
|
|
|
pool.join()
|
|
print "Done!"
|
|
|
|
return results
|
|
|
|
def get_save_dir():
|
|
"""Returns the path to the local saves directory
|
|
* On Windows, at %APPDATA%/.minecraft/saves/
|
|
* On Darwin, at $HOME/Library/Application Support/minecraft/saves/
|
|
* at $HOME/.minecraft/saves/
|
|
|
|
"""
|
|
|
|
savepaths = []
|
|
if "APPDATA" in os.environ:
|
|
savepaths += [os.path.join(os.environ['APPDATA'], ".minecraft", "saves")]
|
|
if "HOME" in os.environ:
|
|
savepaths += [os.path.join(os.environ['HOME'], "Library",
|
|
"Application Support", "minecraft", "saves")]
|
|
savepaths += [os.path.join(os.environ['HOME'], ".minecraft", "saves")]
|
|
|
|
for path in savepaths:
|
|
if os.path.exists(path):
|
|
return path
|
|
|
|
def get_worlds():
|
|
"Returns {world # : level.dat information}"
|
|
ret = {}
|
|
save_dir = get_save_dir()
|
|
for dir in os.listdir(save_dir):
|
|
if dir.startswith("World") and len(dir) == 6:
|
|
world_n = int(dir[-1])
|
|
info = nbt.load(os.path.join(save_dir, dir, "level.dat"))[1]
|
|
info['Data']['path'] = os.path.join(save_dir, dir)
|
|
ret[world_n] = info['Data']
|
|
|
|
return ret
|