Merge branch 'master' into snapshot
This commit is contained in:
116
docs/config.rst
116
docs/config.rst
@@ -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
|
||||||
|
|||||||
@@ -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():
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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()))
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
2
setup.py
2
setup.py
@@ -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")
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
Reference in New Issue
Block a user