0

Merge branch 'master' into snapshot

This commit is contained in:
Andrew Chin
2014-05-07 18:34:45 -04:00
12 changed files with 372 additions and 67 deletions

View File

@@ -545,18 +545,92 @@ values. The valid configuration keys are listed below.
**Default:** ``95`` **Default:** ``95``
``optimizeimg`` ``optimizeimg``
.. warning::
Using image optimizers will increase render times significantly.
This option specifies which additional tools overviewer should use to This option specifies which additional tools overviewer should use to
optimize the filesize of png tiles. optimize the filesize of png tiles.
The tools used must be placed somewhere, where overviewer can find them, for The tools used must be placed somewhere, where overviewer can find them, for
example the "PATH" environment variable or a directory like /usr/bin. example the "PATH" environment variable or a directory like /usr/bin.
This should be an integer between 0 and 3.
* ``1 - Use pngcrush``
* ``2 - Use advdef``
* ``3 - Use pngcrush and advdef (Not recommended)``
Using this option may significantly increase render time, but will make
the resulting tiles smaller, with lossless image quality.
**Default:** ``0`` The option is a list of Optimizer objects, which are then executed in
the order in which they're specified::
from optimizeimages import pngnq, optipng
worlds["world"] = "/path/to/world"
renders["daytime"] = {
"world":"world",
"title":"day",
"rendermode":smooth_lighting,
"optimizeimg":[pngnq(sampling=1), optipng(olevel=3)],
}
Here is a list of supported image optimization programs:
``pngnq``
pngnq quantizes 32-bit RGBA images into 8-bit RGBA palette PNGs. This is
lossy, but reduces filesize significantly. Available settings:
``sampling``
An integer between ``1`` and ``10``, ``1`` samples all pixels, is slow and yields
the best quality. Higher values sample less of the image, which makes
the process faster, but less accurate.
**Default:** ``3``
``dither``
Either the string ``"n"`` for no dithering, or ``"f"`` for Floyd
Steinberg dithering. Dithering helps eliminate colorbanding, sometimes
increasing visual quality.
.. warning::
With pngnq version 1.0 (which is what Ubuntu 12.04 ships), the
dithering option is broken. Only the default, no dithering,
can be specified on those systems.
**Default:** ``"n"``
.. warning::
Because of several PIL bugs, only the most zoomed in level has transparency
when using pngnq. The other zoom levels have all transparency replaced by
black. This is *not* pngnq's fault, as pngnq supports multiple levels of
transparency just fine, it's PIL's fault for not even reading indexed
PNGs correctly.
``optipng``
optipng tunes the deflate algorithm and removes unneeded channels from the PNG,
producing a smaller, lossless output image. It was inspired by pngcrush.
Available settings:
``olevel``
An integer between ``0`` (few optimizations) and ``7`` (many optimizations).
The default should be satisfactory for everyone, higher levels than the default
see almost no benefit.
**Default:** ``2``
``pngcrush``
pngcrush is very slow and not very good, you should use optipng in probably all cases.
However, Overviewer still allows you to use it because we're nice people like that.
Available settings:
``brute``
Either ``True`` or ``False``. Cycles through all compression methods, and is very slow.
.. note::
There is practically no reason to ever use this. optipng will beat pngcrush, and
throwing more CPU time at pngcrush most likely won't help. If you think you need
this option, then you are most likely wrong.
**Default:** ``False``
.. note::
Don't forget to import the optimizers you use in your settings file, as shown in the
example above.
**Default:** ``[]``
``bgcolor`` ``bgcolor``
This is the background color to be displayed behind the map. Its value This is the background color to be displayed behind the map. Its value
@@ -566,13 +640,22 @@ values. The valid configuration keys are listed below.
**Default:** ``#1a1a1a`` **Default:** ``#1a1a1a``
``defaultzoom`` ``defaultzoom``
This value specifies the default zoom level that the map will be opened This value specifies the default zoom level that the map will be
with. It has to be greater than 0. opened with. It has to be greater than 0, which corresponds to the
most zoomed-out level. If you use ``minzoom`` or ``maxzoom``, it
should be between those two.
**Default:** ``1`` **Default:** ``1``
``maxzoom`` ``maxzoom``
This specifies the maximum zoom allowed by the zoom control on the web page. This specifies the maximum, closest in zoom allowed by the zoom
control on the web page. This is relative to 0, the farthest-out
image, so setting this to 8 will allow you to zoom in at most 8
times. This is *not* relative to ``minzoom``, so setting
``minzoom`` will shave off even more levels. If you wish to
specify how many zoom levels to leave off, instead of how many
total to use, use a negative number here. For example, setting
this to -2 will disable the two most zoomed-in levels.
.. note:: .. note::
@@ -583,8 +666,9 @@ values. The valid configuration keys are listed below.
**Default:** Automatically set to most detailed zoom level **Default:** Automatically set to most detailed zoom level
``minzoom`` ``minzoom``
This specifies the minimum zoom allowed by the zoom control on the web page. For This specifies the minimum, farthest away zoom allowed by the zoom
example, setting this to 2 will disable the two most-zoomed out levels. control on the web page. For example, setting this to 2 will
disable the two most zoomed-out levels.
.. note:: .. note::
@@ -694,6 +778,14 @@ values. The valid configuration keys are listed below.
'forcerender': True, 'forcerender': True,
} }
``renderchecks``
This is an integer, and functions as a more complex form of
``forcerender``. Setting it to 1 enables :option:`--check-tiles`
mode, setting it to 2 enables :option:`--forcerender`, and 3 tells
Overviewer to keep this particular render in the output, but
otherwise don't update it. It defaults to 0, which is the usual
update checking mode.
``changelist`` ``changelist``
This is a string. It names a file where it will write out, one per line, the This is a string. It names a file where it will write out, one per line, the
path to tiles that have been updated. You can specify the same file for path to tiles that have been updated. You can specify the same file for

View File

@@ -84,6 +84,8 @@ def main():
help="Tries to locate the texture files. Useful for debugging texture problems.") help="Tries to locate the texture files. Useful for debugging texture problems.")
parser.add_option("-V", "--version", dest="version", parser.add_option("-V", "--version", dest="version",
help="Displays version information and then exits", action="store_true") help="Displays version information and then exits", action="store_true")
parser.add_option("--check-version", dest="checkversion",
help="Fetchs information about the latest version of Overviewer", action="store_true")
parser.add_option("--update-web-assets", dest='update_web_assets', action="store_true", parser.add_option("--update-web-assets", dest='update_web_assets', action="store_true",
help="Update web assets. Will *not* render tiles or update overviewerConfig.js") help="Update web assets. Will *not* render tiles or update overviewerConfig.js")
@@ -141,8 +143,28 @@ def main():
if options.verbose > 0: if options.verbose > 0:
print("Python executable: %r" % sys.executable) print("Python executable: %r" % sys.executable)
print(sys.version) print(sys.version)
if not options.checkversion:
return 0
if options.checkversion:
print("Currently running Minecraft Overviewer %s" % util.findGitVersion()),
print("(%s)" % util.findGitHash()[:7])
try:
import urllib
import json
latest_ver = json.loads(urllib.urlopen("http://overviewer.org/download.json").read())['src']
print("Latest version of Minecraft Overviewer %s (%s)" % (latest_ver['version'], latest_ver['commit'][:7]))
print("See http://overviewer.org/downloads for more information")
except Exception:
print("Failed to fetch latest version info.")
if options.verbose > 0:
import traceback
traceback.print_exc()
else:
print("Re-run with --verbose for more details")
return 1
return 0 return 0
if options.pid: if options.pid:
if os.path.exists(options.pid): if os.path.exists(options.pid):
try: try:
@@ -318,19 +340,24 @@ dir but you forgot to put quotes around the directory, since it contains spaces.
"--check-tiles, and --no-tile-checks. These options conflict.") "--check-tiles, and --no-tile-checks. These options conflict.")
parser.print_help() parser.print_help()
return 1 return 1
def set_renderchecks(checkname, num):
for name, render in config['renders'].iteritems():
if render.get('renderchecks', 0) == 3:
logging.warning(checkname + " ignoring render " + repr(name) + " since it's marked as \"don't render\".")
else:
render['renderchecks'] = num
if options.forcerender: if options.forcerender:
logging.info("Forcerender mode activated. ALL tiles will be rendered") logging.info("Forcerender mode activated. ALL tiles will be rendered")
for render in config['renders'].itervalues(): set_renderchecks("forcerender", 2)
render['renderchecks'] = 2
elif options.checktiles: elif options.checktiles:
logging.info("Checking all tiles for updates manually.") logging.info("Checking all tiles for updates manually.")
for render in config['renders'].itervalues(): set_renderchecks("checktiles", 1)
render['renderchecks'] = 1
elif options.notilechecks: elif options.notilechecks:
logging.info("Disabling all tile mtime checks. Only rendering tiles "+ logging.info("Disabling all tile mtime checks. Only rendering tiles "+
"that need updating since last render") "that need updating since last render")
for render in config['renders'].itervalues(): set_renderchecks("notilechecks", 0)
render['renderchecks'] = 0
if not config['renders']: if not config['renders']:
logging.error("You must specify at least one render in your config file. See the docs if you're having trouble") logging.error("You must specify at least one render in your config file. See the docs if you're having trouble")
@@ -514,6 +541,8 @@ dir but you forgot to put quotes around the directory, since it contains spaces.
if options.pid: if options.pid:
os.remove(options.pid) os.remove(options.pid)
logging.info("Your render has been written to '%s', open index.html to view it" % destdir)
return 0 return 0
def list_worlds(): def list_worlds():

View File

@@ -19,6 +19,7 @@ import logging
import json import json
import sys import sys
import re import re
import urllib2
import Queue import Queue
import multiprocessing import multiprocessing
@@ -30,6 +31,8 @@ from overviewer_core import logger
from overviewer_core import nbt from overviewer_core import nbt
from overviewer_core import configParser, world from overviewer_core import configParser, world
UUID_LOOKUP_URL = 'https://sessionserver.mojang.com/session/minecraft/profile/'
def replaceBads(s): def replaceBads(s):
"Replaces bad characters with good characters!" "Replaces bad characters with good characters!"
bads = [" ", "(", ")"] bads = [" ", "(", ")"]
@@ -132,7 +135,12 @@ def handlePlayers(rset, render, worldpath):
dimension = int(mystdim.group(1)) dimension = int(mystdim.group(1))
else: else:
raise raise
playerdir = os.path.join(worldpath, "players") playerdir = os.path.join(worldpath, "playerdata")
useUUIDs = True
if not os.path.isdir(playerdir):
playerdir = os.path.join(worldpath, "players")
useUUIDs = False
if os.path.isdir(playerdir): if os.path.isdir(playerdir):
playerfiles = os.listdir(playerdir) playerfiles = os.listdir(playerdir)
playerfiles = [x for x in playerfiles if x.endswith(".dat")] playerfiles = [x for x in playerfiles if x.endswith(".dat")]
@@ -152,6 +160,13 @@ def handlePlayers(rset, render, worldpath):
logging.warning("Skipping bad player dat file %r", playerfile) logging.warning("Skipping bad player dat file %r", playerfile)
continue continue
playername = playerfile.split(".")[0] playername = playerfile.split(".")[0]
if useUUIDs:
try:
profile = json.loads(urllib2.urlopen(UUID_LOOKUP_URL + playername.replace('-','')).read())
if 'name' in profile:
playername = profile['name']
except (ValueError, urllib2.URLError):
logging.warning("Unable to get player name for UUID %s", playername)
if isSinglePlayer: if isSinglePlayer:
playername = 'Player' playername = 'Player'
if data['Dimension'] == dimension: if data['Dimension'] == dimension:

View File

@@ -119,7 +119,7 @@ overviewer.util = {
zoom = overviewer.mapView.options.currentTileSet.get('minZoom'); zoom = overviewer.mapView.options.currentTileSet.get('minZoom');
} else { } else {
zoom = parseInt(zoom); zoom = parseInt(zoom);
if (zoom < 0 && zoom + overviewer.mapView.options.currentTileSet.get('maxZoom') >= 0) { if (zoom < 0) {
// if zoom is negative, treat it as a "zoom out from max" // if zoom is negative, treat it as a "zoom out from max"
zoom += overviewer.mapView.options.currentTileSet.get('maxZoom'); zoom += overviewer.mapView.options.currentTileSet.get('maxZoom');
} else { } else {
@@ -127,6 +127,13 @@ overviewer.util = {
zoom = overviewer.mapView.options.currentTileSet.get('defaultZoom'); zoom = overviewer.mapView.options.currentTileSet.get('defaultZoom');
} }
} }
// clip zoom
if (zoom > overviewer.mapView.options.currentTileSet.get('maxZoom'))
zoom = overviewer.mapView.options.currentTileSet.get('maxZoom');
if (zoom < overviewer.mapView.options.currentTileSet.get('minZoom'))
zoom = overviewer.mapView.options.currentTileSet.get('minZoom');
overviewer.map.setZoom(zoom); overviewer.map.setZoom(zoom);
} }
@@ -512,9 +519,9 @@ overviewer.util = {
} }
if (zoom == currTileset.get('maxZoom')) { if (zoom >= currTileset.get('maxZoom')) {
zoom = 'max'; zoom = 'max';
} else if (zoom == currTileset.get('minZoom')) { } else if (zoom <= currTileset.get('minZoom')) {
zoom = 'min'; zoom = 'min';
} else { } else {
// default to (map-update friendly) negative zooms // default to (map-update friendly) negative zooms
@@ -556,7 +563,7 @@ overviewer.util = {
zoom = tsetModel.get('minZoom'); zoom = tsetModel.get('minZoom');
} else { } else {
zoom = parseInt(zoom); zoom = parseInt(zoom);
if (zoom < 0 && zoom + tsetModel.get('maxZoom') >= 0) { if (zoom < 0) {
// if zoom is negative, treat it as a "zoom out from max" // if zoom is negative, treat it as a "zoom out from max"
zoom += tsetModel.get('maxZoom'); zoom += tsetModel.get('maxZoom');
} else { } else {
@@ -565,6 +572,12 @@ overviewer.util = {
} }
} }
// clip zoom
if (zoom > tsetModel.get('maxZoom'))
zoom = tsetModel.get('maxZoom');
if (zoom < tsetModel.get('minZoom'))
zoom = tsetModel.get('minZoom');
overviewer.map.setCenter(latlngcoords); overviewer.map.setCenter(latlngcoords);
overviewer.map.setZoom(zoom); overviewer.map.setZoom(zoom);
var locationmarker = new overviewer.views.LocationIconView(); var locationmarker = new overviewer.views.LocationIconView();

View File

@@ -345,7 +345,7 @@ class ServerAnnounceObserver(Observer):
def update(self, current_value): def update(self, current_value):
super(ServerAnnounceObserver, self).update(current_value) super(ServerAnnounceObserver, self).update(current_value)
if self._need_update(current_value): if self._need_update():
self._send_output('Rendered %d of %d tiles, %d%% complete' % self._send_output('Rendered %d of %d tiles, %d%% complete' %
(self.get_current_value(), self.get_max_value(), (self.get_current_value(), self.get_max_value(),
self.get_percentage())) self.get_percentage()))

View File

@@ -16,37 +16,103 @@
import os import os
import subprocess import subprocess
import shlex import shlex
import logging
pngcrush = "pngcrush" class Optimizer:
optipng = "optipng" binaryname = ""
advdef = "advdef"
def check_programs(level): def __init__(self):
path = os.environ.get("PATH").split(os.pathsep) raise NotImplementedError("I can't let you do that, Dave.")
def optimize(self, img):
raise NotImplementedError("I can't let you do that, Dave.")
def exists_in_path(prog): def fire_and_forget(self, args):
result = filter(lambda x: os.path.exists(os.path.join(x, prog)), path) subprocess.check_call(args)
return len(result) != 0
def check_availability(self):
path = os.environ.get("PATH").split(os.pathsep)
def exists_in_path(prog):
result = filter(lambda x: os.path.exists(os.path.join(x, prog)), path)
return len(result) != 0
if (not exists_in_path(self.binaryname)) and (not exists_in_path(self.binaryname + ".exe")):
raise Exception("Optimization program '%s' was not found!" % self.binaryname)
class NonAtomicOptimizer(Optimizer):
def cleanup(self, img):
os.rename(img + ".tmp", img)
def fire_and_forget(self, args, img):
subprocess.check_call(args)
self.cleanup(img)
class PNGOptimizer:
def __init__(self):
raise NotImplementedError("I can't let you do that, Dave.")
class JPEGOptimizer:
def __init__(self):
raise NotImplementedError("I can't let you do that, Dave.")
class pngnq(NonAtomicOptimizer, PNGOptimizer):
binaryname = "pngnq"
def __init__(self, sampling=3, dither="n"):
if sampling < 1 or sampling > 10:
raise Exception("Invalid sampling value '%d' for pngnq!" % sampling)
if dither not in ["n", "f"]:
raise Exception("Invalid dither method '%s' for pngnq!" % dither)
self.sampling = sampling
self.dither = dither
for prog,l in [(pngcrush,1), (advdef,2)]: def optimize(self, img):
if l <= level: if img.endswith(".tmp"):
if (not exists_in_path(prog)) and (not exists_in_path(prog + ".exe")): extension = ".tmp"
raise Exception("Optimization prog %s for level %d not found!" % (prog, l)) else:
extension = ".png.tmp"
def optimize_image(imgpath, imgformat, optimizeimg): args = [self.binaryname, "-s", str(self.sampling), "-f", "-e", extension, img]
if imgformat == 'png': # Workaround for poopbuntu 12.04 which ships an old broken pngnq
if optimizeimg >= 1: if self.dither != "n":
# we can't do an atomic replace here because windows is terrible args.insert(1, "-Q")
# so instead, we make temp files, delete the old ones, and rename args.insert(2, self.dither)
# the temp files. go windows!
subprocess.Popen([pngcrush, imgpath, imgpath + ".tmp"],
stderr=subprocess.STDOUT, stdout=subprocess.PIPE).communicate()[0]
os.remove(imgpath)
os.rename(imgpath+".tmp", imgpath)
if optimizeimg >= 2: NonAtomicOptimizer.fire_and_forget(self, args, img)
# the "-nc" it's needed to no broke the transparency of tiles
recompress_option = "-z2" if optimizeimg == 2 else "-z4"
subprocess.Popen([advdef, recompress_option,imgpath], stderr=subprocess.STDOUT,
stdout=subprocess.PIPE).communicate()[0]
class pngcrush(NonAtomicOptimizer, PNGOptimizer):
binaryname = "pngcrush"
# really can't be bothered to add some interface for all
# the pngcrush options, it sucks anyway
def __init__(self, brute=False):
self.brute = brute
def optimize(self, img):
args = [self.binaryname, img, img + ".tmp"]
if self.brute == True: # Was the user an idiot?
args.insert(1, "-brute")
NonAtomicOptimizer.fire_and_forget(self, args, img)
class optipng(Optimizer, PNGOptimizer):
binaryname = "optipng"
def __init__(self, olevel=2):
self.olevel = olevel
def optimize(self, img):
Optimizer.fire_and_forget(self, [self.binaryname, "-o" + str(self.olevel), "-quiet", img])
def optimize_image(imgpath, imgformat, optimizers):
for opt in optimizers:
if imgformat == 'png':
if isinstance(opt, PNGOptimizer):
opt.optimize(imgpath)
elif imgformat == 'jpg':
if isinstance(opt, JPEGOptimizer):
opt.optimize(imgpath)

View File

@@ -46,6 +46,7 @@
from settingsValidators import * from settingsValidators import *
import util import util
from observer import ProgressBarObserver, LoggingObserver, JSObserver from observer import ProgressBarObserver, LoggingObserver, JSObserver
from optimizeimages import pngnq, optipng, pngcrush
import platform import platform
import sys import sys
@@ -72,7 +73,7 @@ renders = Setting(required=True, default=util.OrderedDict(),
"imgquality": Setting(required=False, validator=validateImgQuality, default=95), "imgquality": Setting(required=False, validator=validateImgQuality, default=95),
"bgcolor": Setting(required=True, validator=validateBGColor, default="1a1a1a"), "bgcolor": Setting(required=True, validator=validateBGColor, default="1a1a1a"),
"defaultzoom": Setting(required=True, validator=validateDefaultZoom, default=1), "defaultzoom": Setting(required=True, validator=validateDefaultZoom, default=1),
"optimizeimg": Setting(required=True, validator=validateOptImg, default=0), "optimizeimg": Setting(required=True, validator=validateOptImg, default=[]),
"nomarkers": Setting(required=False, validator=validateBool, default=None), "nomarkers": Setting(required=False, validator=validateBool, default=None),
"texturepath": Setting(required=False, validator=validateTexturePath, default=None), "texturepath": Setting(required=False, validator=validateTexturePath, default=None),
"renderchecks": Setting(required=False, validator=validateInt, default=None), "renderchecks": Setting(required=False, validator=validateInt, default=None),

View File

@@ -5,7 +5,9 @@ from collections import namedtuple
import rendermodes import rendermodes
import util import util
from optimizeimages import Optimizer
from world import UPPER_LEFT, UPPER_RIGHT, LOWER_LEFT, LOWER_RIGHT from world import UPPER_LEFT, UPPER_RIGHT, LOWER_LEFT, LOWER_RIGHT
import logging
class ValidationException(Exception): class ValidationException(Exception):
pass pass
@@ -155,8 +157,20 @@ def validateBGColor(color):
return color return color
def validateOptImg(opt): def validateOptImg(optimizers):
return bool(opt) if isinstance(optimizers, (int, long)):
from optimizeimages import pngcrush
logging.warning("You're using a deprecated definition of optimizeimg. We'll do what you say for now, but please fix this as soon as possible.")
optimizers = [pngcrush()]
if not isinstance(optimizers, list):
raise ValidationException("optimizeimg is not a list. Make sure you specify them like [foo()], with square brackets.")
for opt in optimizers:
if not isinstance(opt, Optimizer):
raise ValidationException("Invalid Optimizer!")
opt.check_availability()
return optimizers
def validateTexturePath(path): def validateTexturePath(path):
# Expand user dir in directories strings # Expand user dir in directories strings

View File

@@ -24,6 +24,7 @@ import functools
import time import time
import errno import errno
import stat import stat
import platform
from collections import namedtuple from collections import namedtuple
from itertools import product, izip, chain from itertools import product, izip, chain
@@ -129,6 +130,14 @@ Bounds = namedtuple("Bounds", ("mincol", "maxcol", "minrow", "maxrow"))
# slowest, but SHOULD be specified if this is the first render because # slowest, but SHOULD be specified if this is the first render because
# the scan will forgo tile stat calls. It's also useful for changing # the scan will forgo tile stat calls. It's also useful for changing
# texture packs or other options that effect the output. # texture packs or other options that effect the output.
# 3
# A very special mode. Using this will not actually render
# anything, but will leave this tileset in the resulting
# map. Useful for renders that you want to keep, but not
# update. Since this mode is so simple, it's left out of the
# rest of this discussion.
# #
# For 0 our caller has explicitly requested not to check mtimes on disk to # For 0 our caller has explicitly requested not to check mtimes on disk to
# speed things up. So the mode 0 chunk scan only looks at chunk mtimes and the # speed things up. So the mode 0 chunk scan only looks at chunk mtimes and the
@@ -237,6 +246,13 @@ class TileSet(object):
useful for changing texture packs or other options that effect useful for changing texture packs or other options that effect
the output. the output.
3
A very special mode. Using this will not actually render
anything, but will leave this tileset in the resulting
map. Useful for renders that you want to keep, but not
update. Since this mode is so simple, it's left out of the
rest of this discussion.
imgformat imgformat
A string indicating the output format. Must be one of 'png' or A string indicating the output format. Must be one of 'png' or
'jpeg' 'jpeg'
@@ -246,11 +262,7 @@ class TileSet(object):
relevant in jpeg mode. relevant in jpeg mode.
optimizeimg optimizeimg
an integer indiating optimizations to perform on png outputs. 0 A list of optimizer instances to use.
indicates no optimizations. Only relevant in png mode.
1 indicates pngcrush is run on all output images
2 indicates pngcrush and advdef are run on all output images with advdef -z2
3 indicates pngcrush and advdef are run on all output images with advdef -z4
rendermode rendermode
Perhaps the most important/relevant option: a string indicating the Perhaps the most important/relevant option: a string indicating the
@@ -389,6 +401,11 @@ class TileSet(object):
attribute for later use in iterate_work_items() attribute for later use in iterate_work_items()
""" """
# skip if we're told to
if self.options['renderchecks'] == 3:
return
# REMEMBER THAT ATTRIBUTES ASSIGNED IN THIS METHOD ARE NOT AVAILABLE IN # REMEMBER THAT ATTRIBUTES ASSIGNED IN THIS METHOD ARE NOT AVAILABLE IN
# THE do_work() METHOD (because this is only called in the main process # THE do_work() METHOD (because this is only called in the main process
# not the workers) # not the workers)
@@ -415,15 +432,16 @@ class TileSet(object):
return 1 return 1
def get_phase_length(self, phase): def get_phase_length(self, phase):
"""Returns the number of work items in a given phase, or None if there """Returns the number of work items in a given phase.
is no good estimate.
""" """
# Yeah functional programming! # Yeah functional programming!
# and by functional we mean a bastardized python switch statement
return { return {
0: lambda: self.dirtytree.count_all(), 0: lambda: self.dirtytree.count_all(),
#there is no good way to guess this so just give total count #there is no good way to guess this so just give total count
1: lambda: (4**(self.treedepth+1)-1)/3, 1: lambda: (4**(self.treedepth+1)-1)/3,
2: lambda: self.dirtytree.count_all(), 2: lambda: self.dirtytree.count_all(),
3: lambda: 0,
}[self.options['renderchecks']]() }[self.options['renderchecks']]()
def iterate_work_items(self, phase): def iterate_work_items(self, phase):
@@ -433,6 +451,10 @@ class TileSet(object):
This method returns an iterator over (obj, [dependencies, ...]) This method returns an iterator over (obj, [dependencies, ...])
""" """
# skip if asked to
if self.options['renderchecks'] == 3:
return
# The following block of code implementes the changelist functionality. # The following block of code implementes the changelist functionality.
fd = self.options.get("changelist", None) fd = self.options.get("changelist", None)
if fd: if fd:
@@ -535,6 +557,11 @@ class TileSet(object):
def bgcolorformat(color): def bgcolorformat(color):
return "#%02x%02x%02x" % color[0:3] return "#%02x%02x%02x" % color[0:3]
isOverlay = self.options.get("overlay") or (not any(isinstance(x, rendermodes.Base) for x in self.options.get("rendermode"))) isOverlay = self.options.get("overlay") or (not any(isinstance(x, rendermodes.Base) for x in self.options.get("rendermode")))
# don't update last render time if we're leaving this alone
last_rendertime = self.last_rendertime
if self.options['renderchecks'] != 3:
last_rendertime = self.max_chunk_mtime
d = dict(name = self.options.get('title'), d = dict(name = self.options.get('title'),
zoomLevels = self.treedepth, zoomLevels = self.treedepth,
@@ -545,13 +572,15 @@ class TileSet(object):
bgcolor = bgcolorformat(self.options.get('bgcolor')), bgcolor = bgcolorformat(self.options.get('bgcolor')),
world = self.options.get('worldname_orig') + world = self.options.get('worldname_orig') +
(" - " + self.options.get('dimension')[0] if self.options.get('dimension')[1] != 0 else ''), (" - " + self.options.get('dimension')[0] if self.options.get('dimension')[1] != 0 else ''),
last_rendertime = self.max_chunk_mtime, last_rendertime = last_rendertime,
imgextension = self.imgextension, imgextension = self.imgextension,
isOverlay = isOverlay, isOverlay = isOverlay,
poititle = self.options.get("poititle"), poititle = self.options.get("poititle"),
showlocationmarker = self.options.get("showlocationmarker") showlocationmarker = self.options.get("showlocationmarker")
) )
d['maxZoom'] = min(self.treedepth, d['maxZoom'])
d['minZoom'] = min(max(0, self.options.get("minzoom", 0)), d['maxZoom']) d['minZoom'] = min(max(0, self.options.get("minzoom", 0)), d['maxZoom'])
d['defaultZoom'] = max(d['minZoom'], min(d['defaultZoom'], d['maxZoom']))
if isOverlay: if isOverlay:
d.update({"tilesets": self.options.get("overlay")}) d.update({"tilesets": self.options.get("overlay")})
@@ -760,8 +789,8 @@ class TileSet(object):
# Compare the last modified time of the chunk and tile. If the # Compare the last modified time of the chunk and tile. If the
# tile is older, mark it in a RendertileSet object as dirty. # tile is older, mark it in a RendertileSet object as dirty.
for chunkx, chunkz, chunkmtime in self.regionset.iterate_chunks():
for chunkx, chunkz, chunkmtime in self.regionset.iterate_chunks() if (markall or platform.system() == 'Windows') else self.regionset.iterate_newer_chunks(last_rendertime):
chunkcount += 1 chunkcount += 1
if chunkmtime > max_chunk_mtime: if chunkmtime > max_chunk_mtime:
@@ -892,7 +921,11 @@ class TileSet(object):
try: try:
#quad = Image.open(path[1]).resize((192,192), Image.ANTIALIAS) #quad = Image.open(path[1]).resize((192,192), Image.ANTIALIAS)
src = Image.open(path[1]) src = Image.open(path[1])
# optimizeimg may have converted them to a palette image in the meantime
if src.mode != "RGB" and src.mode != "RGBA":
src = src.convert("RGBA")
src.load() src.load()
quad = Image.new("RGBA", (192, 192), self.options['bgcolor']) quad = Image.new("RGBA", (192, 192), self.options['bgcolor'])
resize_half(quad, src) resize_half(quad, src)
img.paste(quad, path[0]) img.paste(quad, path[0])
@@ -1017,7 +1050,7 @@ class TileSet(object):
if self.options['optimizeimg']: if self.options['optimizeimg']:
optimize_image(tmppath, self.imgextension, self.options['optimizeimg']) optimize_image(tmppath, self.imgextension, self.options['optimizeimg'])
os.utime(tmppath, (max_chunk_mtime, max_chunk_mtime)) os.utime(tmppath, (max_chunk_mtime, max_chunk_mtime))
def _iterate_and_check_tiles(self, path): def _iterate_and_check_tiles(self, path):

View File

@@ -273,7 +273,7 @@ class RegionSet(object):
for x, y, regionfile in self._iterate_regionfiles(): for x, y, regionfile in self._iterate_regionfiles():
# regionfile is a pathname # regionfile is a pathname
self.regionfiles[(x,y)] = regionfile self.regionfiles[(x,y)] = (regionfile, os.path.getmtime(regionfile))
self.empty_chunk = [None,None] self.empty_chunk = [None,None]
logging.debug("Done scanning regions") logging.debug("Done scanning regions")
@@ -459,7 +459,7 @@ class RegionSet(object):
""" """
for (regionx, regiony), regionfile in self.regionfiles.iteritems(): for (regionx, regiony), (regionfile, filemtime) in self.regionfiles.iteritems():
try: try:
mcr = self._get_regionobj(regionfile) mcr = self._get_regionobj(regionfile)
except nbt.CorruptRegionError: except nbt.CorruptRegionError:
@@ -468,6 +468,27 @@ class RegionSet(object):
for chunkx, chunky in mcr.get_chunks(): for chunkx, chunky in mcr.get_chunks():
yield chunkx+32*regionx, chunky+32*regiony, mcr.get_chunk_timestamp(chunkx, chunky) yield chunkx+32*regionx, chunky+32*regiony, mcr.get_chunk_timestamp(chunkx, chunky)
def iterate_newer_chunks(self, mtime):
"""Returns an iterator over all chunk metadata in this world. Iterates
over tuples of integers (x,z,mtime) for each chunk. Other chunk data
is not returned here.
"""
for (regionx, regiony), (regionfile, filemtime) in self.regionfiles.iteritems():
""" SKIP LOADING A REGION WHICH HAS NOT BEEN MODIFIED! """
if (filemtime < mtime):
continue
try:
mcr = self._get_regionobj(regionfile)
except nbt.CorruptRegionError:
logging.warning("Found a corrupt region file at %s,%s. Skipping it.", regionx, regiony)
continue
for chunkx, chunky in mcr.get_chunks():
yield chunkx+32*regionx, chunky+32*regiony, mcr.get_chunk_timestamp(chunkx, chunky)
def get_chunk_mtime(self, x, z): def get_chunk_mtime(self, x, z):
"""Returns a chunk's mtime, or False if the chunk does not exist. This """Returns a chunk's mtime, or False if the chunk does not exist. This
is therefore a dual purpose method. It corrects for the given north is therefore a dual purpose method. It corrects for the given north
@@ -493,7 +514,7 @@ class RegionSet(object):
Coords can be either be global chunk coords, or local to a region Coords can be either be global chunk coords, or local to a region
""" """
regionfile = self.regionfiles.get((chunkX//32, chunkY//32),None) (regionfile,filemtime) = self.regionfiles.get((chunkX//32, chunkY//32),(None, None))
return regionfile return regionfile
def _iterate_regionfiles(self): def _iterate_regionfiles(self):
@@ -537,6 +558,8 @@ class RegionSetWrapper(object):
return self._r.get_chunk(x,z) return self._r.get_chunk(x,z)
def iterate_chunks(self): def iterate_chunks(self):
return self._r.iterate_chunks() return self._r.iterate_chunks()
def iterate_newer_chunks(self,filemtime):
return self._r.iterate_newer_chunks(filemtime)
def get_chunk_mtime(self, x, z): def get_chunk_mtime(self, x, z):
return self._r.get_chunk_mtime(x,z) return self._r.get_chunk_mtime(x,z)
@@ -623,6 +646,11 @@ class RotatedRegionSet(RegionSetWrapper):
x,z = self.rotate(x,z) x,z = self.rotate(x,z)
yield x,z,mtime yield x,z,mtime
def iterate_newer_chunks(self, filemtime):
for x,z,mtime in super(RotatedRegionSet, self).iterate_newer_chunks(filemtime):
x,z = self.rotate(x,z)
yield x,z,mtime
class CroppedRegionSet(RegionSetWrapper): class CroppedRegionSet(RegionSetWrapper):
def __init__(self, rsetobj, xmin, zmin, xmax, zmax): def __init__(self, rsetobj, xmin, zmin, xmax, zmax):
super(CroppedRegionSet, self).__init__(rsetobj) super(CroppedRegionSet, self).__init__(rsetobj)
@@ -646,6 +674,14 @@ class CroppedRegionSet(RegionSetWrapper):
self.xmin <= x <= self.xmax and self.xmin <= x <= self.xmax and
self.zmin <= z <= self.zmax self.zmin <= z <= self.zmax
) )
def iterate_newer_chunks(self, filemtime):
return ((x,z,mtime) for (x,z,mtime) in super(CroppedRegionSet,self).iterate_newer_chunks(filemtime)
if
self.xmin <= x <= self.xmax and
self.zmin <= z <= self.zmax
)
def get_chunk_mtime(self,x,z): def get_chunk_mtime(self,x,z):
if ( if (
self.xmin <= x <= self.xmax and self.xmin <= x <= self.xmax and

View File

@@ -1,6 +1,7 @@
#!/usr/bin/env python #!/usr/bin/env python
import sys import sys
import traceback
# quick version check # quick version check
if not (sys.version_info[0] == 2 and sys.version_info[1] >= 6): if not (sys.version_info[0] == 2 and sys.version_info[1] >= 6):
@@ -272,6 +273,7 @@ class CustomBuild(build):
build.run(self) build.run(self)
print("\nBuild Complete") print("\nBuild Complete")
except Exception: except Exception:
traceback.print_exc(limit=1)
print("\nFailed to build Overviewer!") print("\nFailed to build Overviewer!")
print("Please review the errors printed above and the build instructions") print("Please review the errors printed above and the build instructions")
print("at <http://docs.overviewer.org/en/latest/building/>. If you are") print("at <http://docs.overviewer.org/en/latest/building/>. If you are")

View File

@@ -53,6 +53,10 @@ class FakeRegionset(object):
for (x,z),mtime in self.chunks.iteritems(): for (x,z),mtime in self.chunks.iteritems():
yield x,z,mtime yield x,z,mtime
def iterate_newer_chunks(self, filemtime):
for (x,z),mtime in self.chunks.iteritems():
yield x,z,mtime
def get_chunk_mtime(self, x, z): def get_chunk_mtime(self, x, z):
try: try:
return self.chunks[x,z] return self.chunks[x,z]