diff --git a/overviewer.py b/overviewer.py index 08d9948..7f02289 100755 --- a/overviewer.py +++ b/overviewer.py @@ -18,25 +18,13 @@ import platform import sys +# quick version check if not (sys.version_info[0] == 2 and sys.version_info[1] >= 6): print "Sorry, the Overviewer requires at least Python 2.6 to run" if sys.version_info[0] >= 3: print "and will not run on Python 3.0 or later" sys.exit(1) -isBareConsole = False - -if platform.system() == 'Windows': - try: - import ctypes - GetConsoleProcessList = ctypes.windll.kernel32.GetConsoleProcessList - num = GetConsoleProcessList(ctypes.byref(ctypes.c_int(0)), ctypes.c_int(1)) - if (num == 1): - isBareConsole = True - - except Exception: - pass - import os import os.path import re @@ -44,110 +32,13 @@ import subprocess import multiprocessing import time import logging -from overviewer_core import util - -def doExit(msg=None, code=1, wait=None, consoleMsg=True): - '''Exits Overviewer. If `wait` is None, the default - will be true is 'isBareConsole' is true''' - global isBareConsole - if msg: - print msg - - if wait == None: - if isBareConsole: - if consoleMsg: - print "\n" - print "The Overviewer is a console program. Please open a Windows command prompt" - print "first and run Overviewer from there. Further documentation is available at" - print "http://docs.overviewer.org/\n" - print "Press [Enter] to close this window." - raw_input() - else: - if wait: - if consoleMsg: - print "\n" - print "The Overviewer is a console program. Please open a Windows command prompt" - print "first and run Overviewer from there. Further documentation is available at" - print "http://docs.overviewer.org/\n" - print "Press [Enter] to close this window." - raw_input() - - sys.exit(code) - - -this_dir = util.get_program_path() - -# make sure the c_overviewer extension is available -try: - from overviewer_core import c_overviewer -except ImportError: - ## if this is a frozen windows package, the following error messages about - ## building the c_overviewer extension are not appropriate - if hasattr(sys, "frozen"): - print "Something has gone wrong importing the c_overviewer extension. Please" - print "make sure the 2008 and 2010 redistributable packages from Microsoft" - print "are installed." - doExit() - - - ## try to find the build extension - ext = os.path.join(this_dir, "overviewer_core", "c_overviewer.%s" % ("pyd" if platform.system() == "Windows" else "so")) - if os.path.exists(ext): - print "Something has gone wrong importing the c_overviewer extension. Please" - print "make sure it is up-to-date (clean and rebuild)" - doExit() - - import traceback - traceback.print_exc() - - print "" - print "You need to compile the c_overviewer module to run Minecraft Overviewer." - print "Run `python setup.py build`, or see the README for details." - doExit() - -from overviewer_core import textures - -if hasattr(sys, "frozen"): - pass # we don't bother with a compat test since it should always be in sync -elif "extension_version" in dir(c_overviewer): - # check to make sure the binary matches the headers - if os.path.exists(os.path.join(this_dir, "overviewer_core", "src", "overviewer.h")): - with open(os.path.join(this_dir, "overviewer_core", "src", "overviewer.h")) as f: - lines = f.readlines() - lines = filter(lambda x: x.startswith("#define OVERVIEWER_EXTENSION_VERSION"), lines) - if lines: - l = lines[0] - if int(l.split()[2].strip()) != c_overviewer.extension_version(): - print "Please rebuild your c_overviewer module. It is out of date!" - doExit(code=1, consoleMsg=True) -else: - print "Please rebuild your c_overviewer module. It is out of date!" - doExit() - from optparse import OptionParser + +from overviewer_core import util +from overviewer_core import textures from overviewer_core import optimizeimages, world -#from overviewer_core import googlemap from overviewer_core import configParser, tileset, assetmanager, dispatcher -# definitions of built-in custom modes -# usually because what used to be a mode became an option -# for example, night mode -builtin_custom_rendermodes = {} -# 'night' : { -# 'parent' : 'lighting', -# 'label' : 'Night', -# 'description' : 'like "lighting", except at night', -# 'options' : {'night' : True} -# }, - -# 'smooth-night' : { -# 'parent' : 'smooth-lighting', -# 'label' : 'Smooth Night', -# 'description' : 'like "lighting", except smooth and at night', -# 'options' : {'night' : True} -# }, -# } - helptext = """ %prog [OPTIONS] """ @@ -195,7 +86,6 @@ def configure_logger(loglevel=logging.INFO, verbose=False): logger.setLevel(loglevel) def main(): - # bootstrap the logger with defaults configure_logger() @@ -216,7 +106,6 @@ def main(): #parser.add_option("--forcerender", dest="forcerender", help="Force re-rendering the entire map (or the given regionlist). Useful for re-rendering without deleting it.", action="store_true") #parser.add_option("--stochastic-render", dest="stochastic_render", help="Rerender a non-updated tile randomly, with the given probability (between 0 and 1). Useful for incrementally updating a map with a new mode.", type="float", advanced=True, default=0.0, metavar="PROBABILITY") #parser.add_option("--rendermodes", dest="rendermode", help="Specifies the render types, separated by ',', ':', or '/'. Use --list-rendermodes to list them all.", type="choice", required=True, default=avail_rendermodes[0], listify=True) - parser.add_option("--list-rendermodes", dest="list_rendermodes", action="store_true", help="List available render modes and exit.") #parser.add_option("--rendermode-options", dest="rendermode_options", default={}, advanced=True, help="Used to specify options for different rendermodes. Only useful in a settings.py file") #parser.add_option("--custom-rendermodes", dest="custom_rendermodes", default={}, advanced=True, help="Used to define custom rendermodes. Only useful in a settings.py file") #parser.add_option("--imgformat", dest="imgformat", help="The image output format to use. Currently supported: png(default), jpg.", advanced=True ) @@ -253,59 +142,54 @@ def main(): print "Build machine: %s %s" % (overviewer_version.BUILD_PLATFORM, overviewer_version.BUILD_OS) except ImportError: print "(build info not found)" - pass - doExit(code=0, consoleMsg=False) - - # setup c_overviewer rendermode customs / options - for mode in builtin_custom_rendermodes: - c_overviewer.add_custom_render_mode(mode, builtin_custom_rendermodes[mode]) - # TODO allow custom rendermodes to be specified in a settings.py file - #for mode in options.custom_rendermodes: - # c_overviewer.add_custom_render_mode(mode, options.custom_rendermodes[mode]) - #for mode in options.rendermode_options: - # c_overviewer.set_render_mode_options(mode, options.rendermode_options[mode]) - - - - if options.list_rendermodes: - list_rendermodes() - doExit(code=0, consoleMsg=False) + return 0 if options.check_terrain: import hashlib - from overviewer_core.textures import _find_file - if options.textures_path: - textures._find_file_local_path = options.textures_path + from overviewer_core.textures import Textures + # TODO custom textures path? + tex = Textures() try: - f = _find_file("terrain.png", verbose=True) + f = tex.find_file("terrain.png", verbose=True) except IOError: logging.error("Could not find the file terrain.png") - doExit(code=1, consoleMsg=False) + return 1 h = hashlib.sha1() h.update(f.read()) logging.info("Hash of terrain.png file is: `%s`", h.hexdigest()) - doExit(code=0, consoleMsg=False) + return 0 + + if options.display_config: + # just display the config file and exit + parser.display_config() + return 0 # TODO remove advanced help? needs discussion - #if options.advanced_help: - # parser.advanced_help() - # doExit(code=0, consoleMsg=False) - # TODO right now, we will not let users specify worlds to render on the command line. # TODO in the future, we need to also let worlds be specified on the command line + # if no arguments are provided, print out a helpful message + if len(args) == 0: + # first provide an appropriate error for bare-console users + # that don't provide any options + if util.is_bare_console(): + print "\n" + print "The Overviewer is a console program. Please open a Windows command prompt" + print "first and run Overviewer from there. Further documentation is available at" + print "http://docs.overviewer.org/\n" + else: + # more helpful message for users who know what they're doing + logging.error("You need to give me your world number or directory") + parser.print_help() + list_worlds() + return 1 + # for multiworld, we must specify the *outputdir* on the command line - if len(args) == 1: + elif len(args) == 1: logging.debug("Using %r as the output_directory", args[0]) destdir = os.path.expanduser(args[0]) - - elif len(args) < 1: - logging.error("You need to give me your world number or directory") - parser.print_help() - list_worlds() - doExit(code=1, consoleMsg=True) elif len(args) == 2: # TODO support this usecase worlddir = os.path.expanduser(args[0]) destdir = os.path.expanduser(args[1]) @@ -319,14 +203,7 @@ def main(): if os.path.exists(" ".join(args[start:end])): logging.warning("It looks like you meant to specify \"%s\" as your world dir or your output\n\ dir but you forgot to put quotes around the directory, since it contains spaces." % " ".join(args[start:end])) - doExit(code=1, consoleMsg=False) - - - if options.display_config: - # just display the config file and exit - parser.display_config() - doExit(code=0, consoleMsg=False) - + return 1 # TODO regionlists are per-world #if options.regionlist: @@ -363,11 +240,11 @@ dir but you forgot to put quotes around the directory, since it contains spaces. # except IOError as e: # logging.error("Unable to open file %s to use for changelist." % options.changelist) # logging.error("I/O Error: %s" % e.strerror) - # doExit(code=1, consoleMsg=False) + # return 1 #if options.changelist_format != "auto" and not options.changelist: # logging.error("changelist_format specified without changelist.") - # doExit(code=1, consoleMsg=False) + # return 1 #if options.changelist_format == "auto": # options.changelist_format = "relative" @@ -382,7 +259,7 @@ dir but you forgot to put quotes around the directory, since it contains spaces. tex.generate() except IOError, e: logging.error(str(e)) - doExit(code=1, consoleMsg=False) + return 1 # look at our settings.py file mw_parser = configParser.MultiWorldParser("settings.py") @@ -429,127 +306,7 @@ dir but you forgot to put quotes around the directory, since it contains spaces. dispatch.close() assetMrg.finalize(tilesets) - - sys.exit("early abort") - - # First do world-level preprocessing. This scans the world hierarchy, reads - # in the region files and caches chunk modified times, and determines the - # chunk bounds (max and min in both dimensions) - w = world.World(worlddir) - rset = w.get_regionsets()[0] # use the default - if north_direction == 'auto': - # TODO get this from the asset manager # north_direction = w.persistentData['north_direction'] - options.north_direction = north_direction - - # TODO deal with changed north direction - - # A couple other things we need to figure out about the world: - w.get_regionsets()[0].determine_bounds() - # w.find_true_spawn() - - logging.info("Rendering the following tilesets: %s", ",".join(options.rendermode)) - - bgcolor = (int(options.bg_color[1:3],16), - int(options.bg_color[3:5],16), - int(options.bg_color[5:7],16), - 0) - - # Create the quadtrees. There is one quadtree per rendermode requested, and - # therefore, per output directory hierarchy of tiles. Each quadtree - # individually computes its depth and size. The constructor computes the - # depth of the tree, while the go() method re-arranges tiles if the current - # depth differs from the computed depth. - q = [] - qtree_args = {'depth' : options.zoom, - 'imgformat' : imgformat, - 'imgquality' : options.imgquality, - 'optimizeimg' : optimizeimg, - 'bgcolor' : bgcolor, - 'forcerender' : options.forcerender, - 'rerender_prob' : options.stochastic_render - } - for rendermode in options.rendermode: - if rendermode == 'normal': - qtree = quadtree.QuadtreeGen(rset, destdir, rendermode=rendermode, tiledir='tiles', **qtree_args) - else: - qtree = quadtree.QuadtreeGen(rset, destdir, rendermode=rendermode, **qtree_args) - q.append(qtree) - - # Make sure the quadtrees are the correct depth - for qtree in q: - qtree.check_depth() - - # create the distributed render - r = rendernode.RenderNode(q, options) - # for the pool_initializer - r.builtin_custom_rendermodes = builtin_custom_rendermodes - - # write out the map and web assets - m = googlemap.MapGen(q, configInfo=options) - m.go(options.procs) - - # render the tiles! - r.go(options.procs) - - # finish up the map - m.finalize() - - if options.changelist: - changed=[] - for tile in r.rendered_tiles: - if options.changelist_format=="absolute": - tile=os.path.abspath(tile) - changed.append(tile) - for zl in range(q[0].p - 1): - tile=os.path.dirname(tile) - changed.append("%s.%s" % (tile, imgformat)) - #Quick and nasty way to remove duplicate entries - changed=list(set(changed)) - changed.sort() - for path in changed: - changefile.write("%s\n" % path) - changefile.close() - -def list_rendermodes(): - "Prints out a pretty list of supported rendermodes" - - def print_mode_tree(line_max, mode, prefix='', last=False): - "Prints out a mode tree for the given mode, with an indent." - - try: - info = c_overviewer.get_render_mode_info(mode) - except ValueError: - info = {} - - print prefix + '+-', mode, - - if 'description' in info: - print " " * (line_max - len(prefix) - len(mode) - 2), - print info['description'] - else: - print - - children = c_overviewer.get_render_mode_children(mode) - for child in children: - child_last = (child == children[-1]) - if last: - child_prefix = ' ' - else: - child_prefix = '| ' - print_mode_tree(line_max, child, prefix=prefix + child_prefix, last=child_last) - - avail_rendermodes = c_overviewer.get_render_modes() - line_lengths = {} - parent_modes = [] - for mode in avail_rendermodes: - inherit = c_overviewer.get_render_mode_inheritance(mode) - if not inherit[0] in parent_modes: - parent_modes.append(inherit[0]) - line_lengths[mode] = 2 * len(inherit) + 1 + len(mode) - - line_length = max(line_lengths.values()) - for mode in parent_modes: - print_mode_tree(line_length, mode, last=(mode == parent_modes[-1])) + return 0 def list_worlds(): "Prints out a brief summary of saves found in the default directory" @@ -582,18 +339,14 @@ def list_worlds(): size = "%.2fMB" % (info['SizeOnDisk'] / 1024. / 1024.) print formatString % (name, size, playstamp, timestamp) - if __name__ == "__main__": multiprocessing.freeze_support() try: - main() + ret = main() + util.exit(ret) except Exception, e: - print e - if e.args[0] == "Exiting": - logging.info("Exiting...") - doExit(code=0, wait=False) logging.exception("""An error has occurred. This may be a bug. Please let us know! See http://docs.overviewer.org/en/latest/index.html#help This is the error that occurred:""") - doExit(code=1, consoleMsg=False) + util.exit(1) diff --git a/overviewer_core/__init__.py b/overviewer_core/__init__.py index e69de29..ec4081e 100644 --- a/overviewer_core/__init__.py +++ b/overviewer_core/__init__.py @@ -0,0 +1,71 @@ +# +# Code to check to make sure c_overviewer is built and working +# + +import os.path +import platform +import traceback +import sys + +import util + +def check_c_overviewer(): + """Check to make sure c_overviewer works and is up-to-date. Prints + out a helpful error and returns 1 if something's wrong, returns 0 + otherwise. + """ + root_dir = util.get_program_path() + # make sure the c_overviewer extension is available + try: + import c_overviewer + except ImportError: + ## if this is a frozen windows package, the following error messages about + ## building the c_overviewer extension are not appropriate + if hasattr(sys, "frozen") and platform.system() == 'Windows': + print "Something has gone wrong importing the c_overviewer extension. Please" + print "make sure the 2008 and 2010 redistributable packages from Microsoft" + print "are installed." + return 1 + + ## try to find the build extension + ext = os.path.join(root_dir, "overviewer_core", "c_overviewer.%s" % ("pyd" if platform.system() == "Windows" else "so")) + if os.path.exists(ext): + traceback.print_exc() + print "" + print "Something has gone wrong importing the c_overviewer extension. Please" + print "make sure it is up-to-date (clean and rebuild)" + return 1 + + print "You need to compile the c_overviewer module to run Minecraft Overviewer." + print "Run `python setup.py build`, or see the README for details." + return 1 + + # + # make sure it's up-to-date + # + + if hasattr(sys, "frozen"): + pass # we don't bother with a compat test since it should always be in sync + elif "extension_version" in dir(c_overviewer): + # check to make sure the binary matches the headers + if os.path.exists(os.path.join(root_dir, "overviewer_core", "src", "overviewer.h")): + with open(os.path.join(root_dir, "overviewer_core", "src", "overviewer.h")) as f: + lines = f.readlines() + lines = filter(lambda x: x.startswith("#define OVERVIEWER_EXTENSION_VERSION"), lines) + if lines: + l = lines[0] + if int(l.split()[2].strip()) != c_overviewer.extension_version(): + print "Please rebuild your c_overviewer module. It is out of date!" + return 1 + else: + print "Please rebuild your c_overviewer module. It is out of date!" + return 1 + + # all good! + return 0 + +# only check the module if we're not setup.py +if not sys.argv[0].endswith("setup.py"): + ret = check_c_overviewer() + if ret > 0: + util.exit(ret) diff --git a/overviewer_core/util.py b/overviewer_core/util.py index 156d4de..3f09b4c 100644 --- a/overviewer_core/util.py +++ b/overviewer_core/util.py @@ -21,6 +21,7 @@ import imp import os import os.path import sys +import platform from subprocess import Popen, PIPE import logging from cStringIO import StringIO @@ -88,6 +89,33 @@ def findGitVersion(): except Exception: return "unknown" +def is_bare_console(): + """Returns true if Overviewer is running in a bare console in + Windows, that is, if overviewer wasn't started in a cmd.exe + session. + """ + if platform.system() == 'Windows': + try: + import ctypes + GetConsoleProcessList = ctypes.windll.kernel32.GetConsoleProcessList + num = GetConsoleProcessList(ctypes.byref(ctypes.c_int(0)), ctypes.c_int(1)) + if (num == 1): + return True + + except Exception: + pass + return False + +def exit(ret=0): + """Drop-in replacement for sys.exit that will automatically detect + bare consoles and wait for user input before closing. + """ + if ret and is_bare_console(): + print + print "Press [Enter] to close this window." + raw_input() + sys.exit(ret) + # http://docs.python.org/library/itertools.html def roundrobin(iterables): "roundrobin('ABC', 'D', 'EF') --> A D E B F C"