171 lines
6.0 KiB
Python
171 lines
6.0 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
|
|
|
|
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
|
|
|
|
class WorldRenderer(object):
|
|
"""Renders a world's worth of chunks"""
|
|
def __init__(self, worlddir):
|
|
self.worlddir = worlddir
|
|
self.caves = False
|
|
|
|
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
|
|
|
|
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, 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,),
|
|
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
|