diff --git a/.mailmap b/.mailmap new file mode 100644 index 0000000..23cdf70 --- /dev/null +++ b/.mailmap @@ -0,0 +1,5 @@ +Andrew Brown +Alex Headley +Alex Headley aheadley +Michael Fallows redorkulated +Maciej Malecki Maciej MaƂecki diff --git a/CONTRIBUTORS.rst b/CONTRIBUTORS.rst index fb514d5..4df9493 100644 --- a/CONTRIBUTORS.rst +++ b/CONTRIBUTORS.rst @@ -39,28 +39,46 @@ Short-term Contributions These contributors have made specific changes for a particular bug fix or feature. + * Albireo * arrai + * asmodai + * Mark Barnes * Kyle Brantley * but2002 * Eric Carr * cbarber + * Carter Charbonneau * Alex Cline * Andrew Clunis * CounterPillow + * Johannes Dewender * Michael Fallows + * Ryan Finnie * Stephen Fluin + * Pierre Guinoiseau + * Lucas Hereld * Benjamin Herr * Ryan Hitchman * Jenny * Michael Jensen + * Sean Kilgore * Johan Kiviniemi + * Philip Kovac * Thomas Lake * Maciej Malecki * Ryan McCue + * Zach McCullough + * Mike * Morlok8k + * Adam Novak + * Richard Pastrick * Ryan Rector * Jason Scheirer * Gregory Short * Sam Steele + * stoneLeaf * timwolla + * TJ09 + * untergrundbiber + * Philippe Villiers * Jeffrey Warren diff --git a/contrib/contributors.py b/contrib/contributors.py new file mode 100755 index 0000000..73ee45b --- /dev/null +++ b/contrib/contributors.py @@ -0,0 +1,116 @@ +#!/usr/bin/python2 +"""Update the contributor list + +Alias handling is done by git with .mailmap +New contributors are merged in the short-term list. +Moving them to a "higher" list should be a manual process. +""" + +import fileinput +from subprocess import Popen, PIPE + +def format_contributor(contributor): + return " * {0} {1}".format( + " ".join(contributor["name"]), + contributor["email"]) + + +def main(): + # generate list of contributors + contributors = [] + p_git = Popen(["git", "shortlog", "-se"], stdout=PIPE) + for line in p_git.stdout: + contributors.append({ + 'count': int(line.split("\t")[0].strip()), + 'name': line.split("\t")[1].split()[0:-1], + 'email': line.split("\t")[1].split()[-1] + }) + + # cache listed contributors + old_contributors = [] + with open("CONTRIBUTORS.rst", "r") as contrib_file: + for line in contrib_file: + if "@" in line: + old_contributors.append({ + 'name': line.split()[1:-1], + 'email': line.split()[-1] + }) + + old = map(lambda x: (x['name'], x['email']), old_contributors) + old_emails = map(lambda x: x['email'], old_contributors) + old_names = map(lambda x: x['name'], old_contributors) + + # check which contributors are new + new_contributors = [] + update_mailmap = False + for contributor in contributors: + if (contributor['name'], contributor['email']) in old: + # this exact combination already in the list + pass + elif (contributor['email'] not in old_emails + and contributor['name'] not in old_names): + # name AND email are not in the list + new_contributors.append(contributor) + elif contributor['email'] in old_emails: + # email is listed, but with another name + old_name = filter(lambda x: x['email'] == contributor['email'], + old_contributors)[0]['name'] + print "new alias %s for %s %s ?" % ( + " ".join(contributor['name']), + " ".join(old_name), + contributor['email']) + update_mailmap = True + elif contributor['name'] in old_names: + # probably a new email for a previous contributor + other_mail = filter(lambda x: x['name'] == contributor['name'], + old_contributors)[0]['email'] + print "new email %s for %s %s ?" % ( + contributor['email'], + " ".join(contributor['name']), + other_mail) + update_mailmap = True + if update_mailmap: + print "Please update .mailmap" + + # sort on the last word of the name + new_contributors = sorted(new_contributors, + key=lambda x: x['name'][-1].lower()) + + # show new contributors to be merged to the list + if new_contributors: + print "inserting:" + for contributor in new_contributors: + print format_contributor(contributor) + + # merge with alphabetical (by last part of name) contributor list + i = 0 + short_term_found = False + for line in fileinput.input("CONTRIBUTORS.rst", inplace=1): + if not short_term_found: + print line, + if "Short-term" in line: + short_term_found = True + else: + if i >= len(new_contributors) or "@" not in line: + print line, + else: + listed_name = line.split()[-2].lower() + contributor = new_contributors[i] + # insert all new contributors that fit here + while listed_name > contributor["name"][-1].lower(): + print format_contributor(contributor) + i += 1 + if i < len(new_contributors): + contributor = new_contributors[i] + else: + break + print line, + # append remaining contributors + with open("CONTRIBUTORS.rst", "a") as contrib_file: + while i < len(new_contributors): + contrib_file.write(format_contributor(new_contributors[i]) + "\n") + i += 1 + + +if __name__ == "__main__": + main() diff --git a/docs/config.rst b/docs/config.rst index ac3e35f..b44ca46 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -70,7 +70,7 @@ A more complicated example renders["survivalnight"] = { "world": "survival", - "title": "Survival Daytime", + "title": "Survival Nighttime", "rendermode": smooth_night, "dimension": "overworld", } @@ -461,11 +461,19 @@ values. The valid configuration keys are listed below. **Default:** ``#1a1a1a`` +``base`` + Allows you to specify a remote location for the tile folder, useful if you + rsync your map's images to a remote server. Leave a trailing slash and point + to the location that contains the tile folders for each render, not the + tiles folder itself. For example, if the tile images start at + http://domain.com/map/world_day/ you want to set this to http://domain.com/map/ + .. _option_texture_pack: ``texturepath`` This is a where a specific texture pack can be found to be used during this render. - It can be either a folder or a directory. Its value should be a string. + It can be either a folder or a zip file containing the texture pack. + Its value should be a string. .. _crop: @@ -574,6 +582,14 @@ values. The valid configuration keys are listed below. **Default:** ``[]`` (an empty list) +.. _option_overlay: + +``overlay`` + This specifies which renders that this render will be displayed on top of. + It should be a list of other renders. + + **Default:** ``[]`` (an empty list) + ``showspawn`` This is a boolean, and defaults to ``True``. If set to ``False``, then the spawn icon will not be displayed on the rendered map. @@ -628,6 +644,13 @@ Nether HeightFading Draws a colored overlay on the blocks that fades them out according to their height. + + **Options** + + sealevel + sealevel of the word you're rendering. Note that the default, + 128, is usually *incorrect* for most worlds. You should + probably set this to 64. Default: 128 Depth Only renders blocks between the specified min and max heights. @@ -640,6 +663,17 @@ Depth max highest level of blocks to render. Default: 255 +Exposed + Only renders blocks that are exposed (adjacent to a transparent block). + + **Options** + + mode + when set to 1, inverts the render mode, only drawing unexposed blocks. Default: 0 + +NoFluids + Don't render fluid blocks (water, lava). + EdgeLines Draw edge lines on the back side of blocks, to help distinguish them from the background. @@ -658,6 +692,15 @@ Cave only_lit Only render lit caves. Default: False +Hide + Hide blocks based on blockid. Blocks hidden in this way will be + treated exactly the same as air. + + **Options** + + minerals + A list of block ids, or (blockid, data) tuples to hide. + DepthTinting Tint blocks a color according to their depth (height) from bedrock. Useful mainly for cave renders. @@ -687,16 +730,16 @@ ClearBase Forces the background to be transparent. Use this in place of Base for rendering pure overlays. - .. warning:: - - Overlays are currently not functional in this branch of code. We are - working on them. Please inquire in :ref:`IRC` for more information. - SpawnOverlay Color the map red in areas where monsters can spawn. Either use this on top of other modes, or on top of ClearBase to create a pure overlay. +SlimeOverlay + Color the map green in chunks where slimes can spawn. Either use + this on top of other modes, or on top of ClearBase to create a + pure overlay. + MineralOverlay Color the map according to what minerals can be found underneath. Either use this on top of other modes, or on top of diff --git a/docs/design/designdoc.rst b/docs/design/designdoc.rst index f319e2e..a69a099 100644 --- a/docs/design/designdoc.rst +++ b/docs/design/designdoc.rst @@ -17,7 +17,7 @@ So let's get started! .. note:: - This page is still under construction + This page is continually under construction .. contents:: @@ -82,7 +82,7 @@ pre-rendered sprite (a small image). The basic idea is to iterate over the blocks of the world and draw these sprites to the appropriate location on the map. -These are the high-level tasks The Overviewer must preform in rendering a map: +These are the high-level tasks The Overviewer must perform in rendering a map: 1. Render each block sprite from the textures 2. Scan the chunks of the world and determine which tiles need rendering @@ -143,7 +143,7 @@ transformations can be chained together simply by multiplying the transformation matrices together, only one transformation is actually done. This can be seen in the function -:func:`overviewer_core.textures.transform_image`. It preforms three steps: +:func:`overviewer_core.textures.transform_image`. It performs three steps: 1. The texture is re-sized to 17 by 17 pixels. This is done because the diagonal of a square with sides 17 is approximately 24, which is the target size for diff --git a/docs/running.rst b/docs/running.rst index 4d5fc42..2e5cb78 100644 --- a/docs/running.rst +++ b/docs/running.rst @@ -246,7 +246,7 @@ If you want or need to provide your own textures, you have several options: overviewer.exe. * Specify any terrain.png or texture pack you want with the - :ref:`texture_pack` option. + :ref:`texturepath` option. If you copy your world before you render it ------------------------------------------- diff --git a/docs/signs.rst b/docs/signs.rst index d10e755..8384694 100644 --- a/docs/signs.rst +++ b/docs/signs.rst @@ -20,51 +20,100 @@ Filter Functions ---------------- A filter function is a python function that is used to figure out if a given POI -should be part of a markerSet of not. The function should accept one argument -(a dictionary, also know as an associative array), and return a boolean:: +should be part of a markerSet of not, and to control how it is displayed. +The function should accept one argument (a dictionary, also know as an associative +array), and return a string representing the text to be displayed. For example:: def signFilter(poi): - "All signs" - return poi['id'] == 'Sign' + if poi['id'] == 'Sign': + return "\n".join([poi['Text1'], poi['Text2'], poi['Text3'], poi['Text4']]) + +If a POI doesn't match, the filter can return None (which is the default if a python +functions runs off the end without an explicit 'return'). The single argument will either a TileEntity, or an Entity taken directly from -the chunk file. In this example, this function returns true only if the type -of entity is a sign. For more information of TileEntities and Entities, see +the chunk file. It could also be a special entity representing a player's location +or a player's spawn. See below for more details. + +In this example, this function returns all 4 lines from the sign +if the entity is a sign. +For more information of TileEntities and Entities, see the `Chunk Format `_ page on the Minecraft Wiki. -.. note:: - The doc string ("All signs" in this example) is important. It is the label - that appears in your rendered map +A more complicated filter function can construct a more customized display text:: -A more advanced filter may also look at other entity fields, such as the sign text:: + def chestFilter(poi): + if poi['id'] == "Chest": + return "Chest with %d items" % len(poi['Items']) - def goldFilter(poi): - "Gold" - return poi['id'] == 'Sign' and (\ - 'gold' in poi['Text1'] or - 'gold' in poi['Text2']) - -This looks for the word 'gold' in either the first or second line of the signtext. Since writing these filters can be a little tedious, a set of predefined filters functions are provided. See the :ref:`predefined_filter_functions` section for details. + +Special POIs +------------ + +There are currently two special types of POIs. They each have a special id: + +PlayerSpawn + Used to indicate the spawn location of a player. The player's name is set + in the ``EntityId`` key, and the location is in the x,y,z keys + +Player + Used to indicate the last known location of a player. The player's name is set + in the ``EntityId`` key, and the location is in the x,y,z keys. + +.. note:: + The player location is taken from level.dat (in the case of a single-player world) + or the player.dat files (in the case of a multi-player server). The locations are + only written to these files when the world is saved, so this won't give you real-time + player location information. + +Here's an example that displays icons for each player:: + + def playerIcons(poi): + if poi['id'] == 'Player': + poi['icon'] = "http://overviewer.org/avatar/%s" % poi['EntityId'] + return "Last known location for %s" % poi['EntityId'] + +Note how each POI can get a different icon by setting ``poi['icon']`` + Render Dictionary Key --------------------- Each render can specify a list of zero or more filter functions. Each of these filter functions become a selectable item in the 'Signs' drop-down menu in the -rendered map. For example:: +rendered map. Previously, this used to be a list of functions. Now it is a list +of dictionaries. For example:: renders['myrender'] = { 'world': 'myworld', 'title': "Example", - 'markers': [allFilter, anotherFilter], + 'markers': [dict(name="All signs", filterFunction=signFilter), + dict(name="Chests", filterFunction=chestFilter, icon="chest.png", createInfoWindow=False)] } +The following keys are accepted in the marker dictionary: + +``name`` + This is the text that is displayed in the 'Signs' dropdown. + +``filterFunction`` + This is the filter function. It must accept at least 1 argument (the POI to filter), + and it must return either None or a string. + +``icon`` + Optional. Specifies the icon to use for POIs in this group. If omitted, it defaults + to a signpost icon. Note that each POI can have different icon by setting the key 'icon' + on the POI itself (this can be done by modifying the POI in the filter function. See the + example above) + +``createInfoWindow`` + Optional. Specifies whether or not the icon displays an info window on click. Defaults to True Generating the POI Markers diff --git a/overviewer.py b/overviewer.py index 986d59a..ab4e4ac 100755 --- a/overviewer.py +++ b/overviewer.py @@ -89,6 +89,8 @@ def main(): help="Print less output. You can specify this option multiple times.") parser.add_option("-v", "--verbose", dest="verbose", action="count", default=0, help="Print more output. You can specify this option multiple times.") + parser.add_option("--simple-output", dest="simple", action="store_true", default=False, + help="Use a simple output format, with no colors or progress bars") # create a group for "plugin exes" (the concept of a plugin exe is only loosly defined at this point) exegroup = OptionGroup(parser, "Other Scripts", @@ -104,9 +106,9 @@ def main(): if options.genpoi: # remove the "--genpoi" option from sys.argv before running genPI sys.argv.remove("--genpoi") - sys.path.append(".") - g = __import__("genPOI", {}, {}) - g.main() + #sys.path.append(".") + g = __import__("overviewer_core.aux_files", {}, {}, ["genPOI"]) + g.genPOI.main() return 0 if options.help: parser.print_help() @@ -114,7 +116,8 @@ def main(): # re-configure the logger now that we've processed the command line options logger.configure(logging.INFO + 10*options.quiet - 10*options.verbose, - options.verbose > 0) + verbose=options.verbose > 0, + simple=options.simple) ########################################################################## # This section of main() runs in response to any one-time options we have, @@ -233,7 +236,6 @@ dir but you forgot to put quotes around the directory, since it contains spaces. return 1 # Parse the config file - mw_parser = configParser.MultiWorldParser() mw_parser.parse(options.config) # Add in the command options here, perhaps overriding values specified in @@ -244,8 +246,12 @@ dir but you forgot to put quotes around the directory, since it contains spaces. # Now parse and return the validated config try: config = mw_parser.get_validated_config() - except Exception: - logging.exception("An error was encountered with your configuration. See the info below.") + except Exception as ex: + if options.verbose: + logging.exception("An error was encountered with your configuration. See the info below.") + else: # no need to print scary traceback! just + logging.error("An error was encountered with your configuration.") + logging.error(str(ex)) return 1 @@ -302,6 +308,20 @@ dir but you forgot to put quotes around the directory, since it contains spaces. if render.get('forcerender', False): render['renderchecks'] = 2 + # check if overlays are set, if so, make sure that those renders exist + if render.get('overlay', []) != []: + for x in render.get('overlay'): + if x != rname: + try: + renderLink = config['renders'][x] + except KeyError: + logging.error("Render %s's overlay is '%s', but I could not find a corresponding entry in the renders dictionary.", + rname, x) + return 1 + else: + logging.error("Render %s's overlay contains itself.", rname) + return 1 + destdir = config['outputdir'] if not destdir: logging.error("You must specify the output directory in your config file.") @@ -401,9 +421,9 @@ dir but you forgot to put quotes around the directory, since it contains spaces. # only pass to the TileSet the options it really cares about render['name'] = render_name # perhaps a hack. This is stored here for the asset manager - tileSetOpts = util.dict_subset(render, ["name", "imgformat", "renderchecks", "rerenderprob", "bgcolor", "imgquality", "optimizeimg", "rendermode", "worldname_orig", "title", "dimension", "changelist","showspawn"]) + tileSetOpts = util.dict_subset(render, ["name", "imgformat", "renderchecks", "rerenderprob", "bgcolor", "imgquality", "optimizeimg", "rendermode", "worldname_orig", "title", "dimension", "changelist","showspawn", "overlay","base"]) tileSetOpts.update({"spawn": w.find_true_spawn()}) # TODO find a better way to do this - tset = tileset.TileSet(rset, assetMrg, tex, tileSetOpts, tileset_dir) + tset = tileset.TileSet(w, rset, assetMrg, tex, tileSetOpts, tileset_dir) tilesets.append(tset) # Do tileset preprocessing here, before we start dispatching jobs diff --git a/overviewer_core/assetmanager.py b/overviewer_core/assetmanager.py index 6407c1d..983f1e3 100644 --- a/overviewer_core/assetmanager.py +++ b/overviewer_core/assetmanager.py @@ -87,6 +87,7 @@ directory. dump['CONST']['image'] = { 'defaultMarker': 'signpost.png', 'signMarker': 'signpost_icon.png', + 'bedMarker': 'bed.png', 'compass': 'compass_upper-left.png', 'spawnMarker': 'http://google-maps-icons.googlecode.com/files/home.png', 'queryMarker': 'http://google-maps-icons.googlecode.com/files/regroup.png' diff --git a/overviewer_core/aux_files/__init__.py b/overviewer_core/aux_files/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/genPOI.py b/overviewer_core/aux_files/genPOI.py similarity index 52% rename from genPOI.py rename to overviewer_core/aux_files/genPOI.py index 66a8360..f67e556 100755 --- a/genPOI.py +++ b/overviewer_core/aux_files/genPOI.py @@ -17,6 +17,7 @@ markers.js holds a list of which markerSets are attached to each tileSet import os import logging import json +import sys from optparse import OptionParser from overviewer_core import logger @@ -25,7 +26,8 @@ from overviewer_core import configParser, world def handleSigns(rset, outputdir, render, rname): - + + # if we're already handled the POIs for this region regionset, do nothing if hasattr(rset, "_pois"): return @@ -39,10 +41,64 @@ def handleSigns(rset, outputdir, render, rname): rset._pois['TileEntities'] += data['TileEntities'] rset._pois['Entities'] += data['Entities'] + logging.info("Done.") + +def handlePlayers(rset, render, worldpath): + if not hasattr(rset, "_pois"): + rset._pois = dict(TileEntities=[], Entities=[]) + + # only handle this region set once + if 'Players' in rset._pois: + return + dimension = {'overworld': 0, + 'nether': -1, + 'end': 1, + 'default': 0}[render['dimension']] + playerdir = os.path.join(worldpath, "players") + if os.path.isdir(playerdir): + playerfiles = os.listdir(playerdir) + isSinglePlayer = False + else: + playerfiles = [os.path.join(worldpath, "level.dat")] + isSinglePlayer = True + + rset._pois['Players'] = [] + for playerfile in playerfiles: + try: + data = nbt.load(os.path.join(playerdir, playerfile))[1] + if isSinglePlayer: + data = data['Data']['Player'] + except IOError: + logging.warning("Skipping bad player dat file %r", playerfile) + continue + playername = playerfile.split(".")[0] + if isSinglePlayer: + playername = 'Player' + if data['Dimension'] == dimension: + # Position at last logout + data['id'] = "Player" + data['EntityId'] = playername + data['x'] = int(data['Pos'][0]) + data['y'] = int(data['Pos'][1]) + data['z'] = int(data['Pos'][2]) + rset._pois['Players'].append(data) + if "SpawnX" in data and dimension == 0: + # Spawn position (bed or main spawn) + spawn = {"id": "PlayerSpawn", + "EntityId": playername, + "x": data['SpawnX'], + "y": data['SpawnY'], + "z": data['SpawnZ']} + rset._pois['Players'].append(spawn) def main(): - helptext = """genPOI - %prog --config=""" + + if os.path.basename(sys.argv[0]) == """genPOI.py""": + helptext = """genPOI.py + %prog --config= [--quiet]""" + else: + helptext = """genPOI + %prog --genpoi --config= [--quiet]""" logger.configure() @@ -97,26 +153,47 @@ def main(): return 1 for f in render['markers']: - markersets.add((f, rset)) - name = f.__name__ + hex(hash(f))[-4:] + "_" + hex(hash(rset))[-4:] + d = dict(icon="signpost_icon.png", createInfoWindow=True) + d.update(f) + markersets.add(((d['name'], d['filterFunction']), rset)) + name = f['name'].replace(" ","_") + hex(hash(f['filterFunction']))[-4:] + "_" + hex(hash(rset))[-4:] try: l = markers[rname] - l.append(dict(groupName=name, displayName = f.__doc__)) + l.append(dict(groupName=name, displayName = f['name'], icon=d['icon'], createInfoWindow=d['createInfoWindow'])) except KeyError: - markers[rname] = [dict(groupName=name, displayName=f.__doc__),] + markers[rname] = [dict(groupName=name, displayName=f['name'], icon=d['icon'], createInfoWindow=d['createInfoWindow']),] handleSigns(rset, os.path.join(destdir, rname), render, rname) + handlePlayers(rset, render, worldpath) logging.info("Done scanning regions") logging.info("Writing out javascript files") markerSetDict = dict() for (flter, rset) in markersets: # generate a unique name for this markerset. it will not be user visible - name = flter.__name__ + hex(hash(flter))[-4:] + "_" + hex(hash(rset))[-4:] - markerSetDict[name] = dict(created=False, raw=[]) + filter_name = flter[0] + filter_function = flter[1] + + name = filter_name.replace(" ","_") + hex(hash(filter_function))[-4:] + "_" + hex(hash(rset))[-4:] + markerSetDict[name] = dict(created=False, raw=[], name=filter_name) for poi in rset._pois['TileEntities']: - if flter(poi): - markerSetDict[name]['raw'].append(poi) + result = filter_function(poi) + if result: + d = dict(x=poi['x'], y=poi['y'], z=poi['z'], text=result) + if "icon" in poi: + d.update({"icon": poi['icon']}) + if "createInfoWindow" in poi: + d.update({"createInfoWindow": poi['createInfoWindow']}) + markerSetDict[name]['raw'].append(d) + for poi in rset._pois['Players']: + result = filter_function(poi) + if result: + d = dict(x=poi['x'], y=poi['y'], z=poi['z'], text=result) + if "icon" in poi: + d.update({"icon": poi['icon']}) + if "createInfoWindow" in poi: + d.update({"createInfoWindow": poi['createInfoWindow']}) + markerSetDict[name]['raw'].append(d) #print markerSetDict with open(os.path.join(destdir, "markersDB.js"), "w") as output: diff --git a/overviewer_core/data/js_src/util.js b/overviewer_core/data/js_src/util.js index 3d6c36e..96e3965 100644 --- a/overviewer_core/data/js_src/util.js +++ b/overviewer_core/data/js_src/util.js @@ -67,6 +67,8 @@ overviewer.util = { signs.registerEvents(signs); } + var overlayControl = new overviewer.views.OverlayControlView(); + var spawnmarker = new overviewer.views.SpawnIconView(); // Update coords on mousemove @@ -85,6 +87,9 @@ overviewer.util = { compass.render(); spawnmarker.render(); + // update list of spawn overlays + overlayControl.render(); + // re-center on the last viewport var currentWorldView = overviewer.mapModel.get("currentWorldView"); if (currentWorldView.options.lastViewport) { @@ -117,8 +122,6 @@ overviewer.util = { }); - var worldSelector = new overviewer.views.WorldSelectorView({tagName:'DIV'}); - overviewer.collections.worlds.bind("add", worldSelector.render, worldSelector); // hook up some events @@ -129,6 +132,11 @@ overviewer.util = { // Jump to the hash if given overviewer.util.initHash(); + // create this control after initHash so it can correctly select the current world + var worldSelector = new overviewer.views.WorldSelectorView({tagName:'DIV'}); + overviewer.collections.worlds.bind("add", worldSelector.render, worldSelector); + + overviewer.util.initializeMarkers(); /* diff --git a/overviewer_core/data/js_src/views.js b/overviewer_core/data/js_src/views.js index e66267e..8b58c1a 100644 --- a/overviewer_core/data/js_src/views.js +++ b/overviewer_core/data/js_src/views.js @@ -4,8 +4,19 @@ overviewer.views= {} overviewer.views.WorldView = Backbone.View.extend({ initialize: function(opts) { this.options.mapTypes = []; + this.options.overlayMapTypes = []; this.options.mapTypeIds = []; + this.options.overlayMapTypeIds = []; + + var curTileSet = this.model.get("tileSets").at(0); + var spawn = curTileSet.get("spawn"); + if (spawn == "false") { + var spawn = [0,64,0]; + } + this.options.lastViewport = [spawn[0],spawn[1],spawn[2],curTileSet.get("defaultZoom")]; + this.model.get("tileSets").each(function(tset, index, list) { + // ignore overlays: var ops = { getTileUrl: overviewer.gmap.getTileUrlGenerator(tset.get("path"), tset.get("base"), tset.get("imgextension")), 'tileSize': new google.maps.Size( @@ -20,11 +31,24 @@ overviewer.views.WorldView = Backbone.View.extend({ newMapType.shortname = tset.get("name"); newMapType.alt = "Minecraft " + tset.get("name") + " Map"; newMapType.projection = new overviewer.classes.MapProjection(); - - this.options.mapTypes.push(newMapType); - this.options.mapTypeIds.push(overviewerConfig.CONST.mapDivId + this.model.get("name") + tset.get("name")); + newMapType._ov_tileSet = tset; + + if (tset.get("isOverlay")) { + newMapType.tiles = tset.get("tilesets"); + this.options.overlayMapTypes.push(newMapType); + this.options.overlayMapTypeIds.push(overviewerConfig.CONST.mapDivId + this.model.get("name") + tset.get("name")); + } else { + this.options.mapTypes.push(newMapType); + this.options.mapTypeIds.push(overviewerConfig.CONST.mapDivId + this.model.get("name") + tset.get("name")); + } }, this); + + this.model.get("tileSets").each(function(tset, index, list) { + // ignore non-overlays: + if (!tset.get("isOverlay")) { return; }; + + }); }, }); @@ -33,6 +57,8 @@ overviewer.views.WorldView = Backbone.View.extend({ overviewer.views.WorldSelectorView = Backbone.View.extend({ initialize: function() { if(overviewer.collections.worldViews.length > 1) { + $(this.el).addClass("customControl"); + // a div will have already been created for us, we just // need to register it with the google maps control var selectBox = document.createElement('select'); @@ -40,6 +66,9 @@ overviewer.views.WorldSelectorView = Backbone.View.extend({ var o = document.createElement("option"); o.value = elem.model.get("name"); o.innerHTML = elem.model.get("name"); + if (elem.model == overviewer.mapModel.get("currentWorldView").model) { + o.selected=true; + } $(o).data("viewObj", elem); selectBox.appendChild(o); @@ -139,17 +168,16 @@ overviewer.views.GoogleMapView = Backbone.View.extend({ var curWorld = this.model.get("currentWorldView").model; var curTset = curWorld.get("tileSets").at(0); + var spawn = curTset.get("spawn"); + if (spawn == "false") { + var spawn = [0,64,0]; + } + var mapcenter = overviewer.util.fromWorldToLatLng( + spawn[0], + spawn[1], + spawn[2], + curTset); - /* - var defaultCenter = overviewer.util.fromWorldToLatLng( - overviewerConfig.map.center[0], - overviewerConfig.map.center[1], - overviewerConfig.map.center[2], - curTset.get("defaultZoom")); - */ - var lat = 0.62939453125;// TODO defaultCenter.lat(); - var lng = 0.38525390625; // TODO defaultCenter.lng(); - var mapcenter = new google.maps.LatLng(lat, lng); this.options.mapTypes=[]; this.options.mapTypeIds=[]; @@ -208,7 +236,7 @@ overviewer.views.GoogleMapView = Backbone.View.extend({ var gmapCurrent = overviewer.map.getMapTypeId(); for (id in currentWorldView.options.mapTypeIds) { if (currentWorldView.options.mapTypeIds[id] == gmapCurrent) { - this.options.currentTileSet = currentWorldView.model.get("tileSets").at(id); + this.options.currentTileSet = currentWorldView.options.mapTypes[id]._ov_tileSet; } } @@ -220,6 +248,114 @@ overviewer.views.GoogleMapView = Backbone.View.extend({ }); +/** + * OverlayControlView + */ +overviewer.views.OverlayControlView = Backbone.View.extend({ + /** OverlayControlVIew::initialize + */ + initialize: function(opts) { + $(this.el).addClass("customControl"); + overviewer.map.controls[google.maps.ControlPosition.TOP_RIGHT].push(this.el); + }, + registerEvents: function(me) { + overviewer.mapModel.bind("change:currentWorldView", me.render, me); + }, + + /** + * OverlayControlView::render + */ + render: function() { + this.el.innerHTML=""; + + // hide all visible overlays: + overviewer.map.overlayMapTypes.clear() + + // if this world has no overlays, don't create this control + var mapTypes = overviewer.mapModel.get('currentWorldView').options.overlayMapTypes; + if (mapTypes.length == 0) { return; } + + var controlText = document.createElement('DIV'); + controlText.innerHTML = "Overlays"; + + var controlBorder = document.createElement('DIV'); + $(controlBorder).addClass('top'); + this.el.appendChild(controlBorder); + controlBorder.appendChild(controlText); + + var dropdownDiv = document.createElement('DIV'); + $(dropdownDiv).addClass('dropDown'); + this.el.appendChild(dropdownDiv); + dropdownDiv.innerHTML=''; + + $(controlText).click(function() { + $(controlBorder).toggleClass('top-active'); + $(dropdownDiv).toggle(); + }); + + var currentTileSetPath = overviewer.mapView.options.currentTileSet.get('path'); + + for (i in mapTypes) { + var mt = mapTypes[i]; + // if this overlay specifies a list of valid tilesets, then skip over any invalid tilesets + if ((mt.tiles.length > 0) && (mt.tiles.indexOf(currentTileSetPath) ==-1)) { + continue; + } + this.addItem({label: mt.name, + name: mt.name, + mt: mt, + + action: function(this_item, checked) { + if (checked) { + overviewer.map.overlayMapTypes.push(this_item.mt); + } else { + var idx_to_delete = -1; + overviewer.map.overlayMapTypes.forEach(function(e, j) { + if (e == this_item.mt) { + idx_to_delete = j; + } + }); + if (idx_to_delete >= 0) { + overviewer.map.overlayMapTypes.removeAt(idx_to_delete); + } + } + + } + }); + } + + + }, + + addItem: function(item) { + var itemDiv = document.createElement('div'); + var itemInput = document.createElement('input'); + itemInput.type='checkbox'; + + // if this overlay is already visible, set the checkbox + // to checked + overviewer.map.overlayMapTypes.forEach(function(e, j) { + if (e == item.mt) { + itemInput.checked=true; + } + }); + + // give it a name + $(itemInput).attr("_mc_overlayname", item.name); + jQuery(itemInput).click((function(local_item) { + return function(e) { + item.action(local_item, e.target.checked); + }; + })(item)); + + this.$(".dropDown")[0].appendChild(itemDiv); + itemDiv.appendChild(itemInput); + var textNode = document.createElement('text'); + textNode.innerHTML = item.label + '
'; + + itemDiv.appendChild(textNode); + } +}); /** @@ -285,7 +421,7 @@ overviewer.views.SignControlView = Backbone.View.extend({ //var dataRoot = overviewer.collections.markerInfo[curMarkerSet]; var dataRoot = markers[curMarkerSet]; - this.el.innerHTML="" + this.el.innerHTML=""; // if we have no markerSets for this tileset, do nothing: if (!dataRoot) { return; } @@ -322,7 +458,6 @@ overviewer.views.SignControlView = Backbone.View.extend({ }}); } - iconURL = overviewerConfig.CONST.image.signMarker; //dataRoot['markers'] = []; // for (i in dataRoot) { @@ -330,16 +465,25 @@ overviewer.views.SignControlView = Backbone.View.extend({ if (!markersDB[groupName].created) { for (j in markersDB[groupName].raw) { var entity = markersDB[groupName].raw[j]; + if (entity['icon']) { + iconURL = entity['icon']; + } else { + iconURL = dataRoot[i].icon; + } var marker = new google.maps.Marker({ 'position': overviewer.util.fromWorldToLatLng(entity.x, entity.y, entity.z, overviewer.mapView.options.currentTileSet), 'map': overviewer.map, - 'title': jQuery.trim(entity.Text1 + "\n" + entity.Text2 + "\n" + entity.Text3 + "\n" + entity.Text4), + 'title': jQuery.trim(entity.text), 'icon': iconURL, 'visible': false }); - if (entity['id'] == 'Sign') { + if(entity['createInfoWindow'] == true) { overviewer.util.createMarkerInfoWindow(marker); + } else { + if(dataRoot[i].createInfoWindow == true) { + overviewer.util.createMarkerInfoWindow(marker); + } } jQuery.extend(entity, {markerObj: marker}); } @@ -368,13 +512,13 @@ overviewer.views.SignControlView = Backbone.View.extend({ var textNode = document.createElement('text'); if(item.icon) { textNode.innerHTML = '' + item.label + '
'; + item.icon + '">' + item.label + ' 
'; } else { - textNode.innerHTML = item.label + '
'; + textNode.innerHTML = item.label + ' 
'; } itemDiv.appendChild(textNode); - + itemDiv.style.whiteSpace = "nowrap"; }, }); diff --git a/overviewer_core/data/web_assets/bed.png b/overviewer_core/data/web_assets/bed.png new file mode 100644 index 0000000..3645ac0 Binary files /dev/null and b/overviewer_core/data/web_assets/bed.png differ diff --git a/overviewer_core/data/web_assets/overviewer.css b/overviewer_core/data/web_assets/overviewer.css index e1f1527..fff3458 100644 --- a/overviewer_core/data/web_assets/overviewer.css +++ b/overviewer_core/data/web_assets/overviewer.css @@ -40,6 +40,17 @@ body { font-family: Arial, sans-serif; } +.customControl > select { + font-size: 12px; + line-height: 160%; + text-align: center; + + border: 1px solid #A9BBDF; + border-radius: 2px 2px; + box-shadow: rgba(0, 0, 0, 0.347656) 2px 2px 3px; + +} + .customControl > div.top { font-size: 12px; line-height: 160%; diff --git a/overviewer_core/logger.py b/overviewer_core/logger.py index 96850cf..4fd36a3 100644 --- a/overviewer_core/logger.py +++ b/overviewer_core/logger.py @@ -254,7 +254,7 @@ class ANSIColorFormatter(HighlightingFormatter): # No coloring if it's not to be highlighted or colored return logging.Formatter.format(self, record) -def configure(loglevel=logging.INFO, verbose=False): +def configure(loglevel=logging.INFO, verbose=False, simple=False): """Configures the root logger to our liking For a non-standard loglevel, pass in the level with which to configure the handler. @@ -267,15 +267,17 @@ def configure(loglevel=logging.INFO, verbose=False): logger = logging.getLogger() - outstream = sys.stderr + outstream = sys.stdout + if simple: + formatter = DumbFormatter(verbose) - if platform.system() == 'Windows': + elif platform.system() == 'Windows': # Our custom output stream processor knows how to deal with select ANSI # color escape sequences - outstream = WindowsOutputStream() + outstream = WindowsOutputStream(outstream) formatter = ANSIColorFormatter(verbose) - elif sys.stderr.isatty(): + elif outstream.isatty(): # terminal logging with ANSI color formatter = ANSIColorFormatter(verbose) diff --git a/overviewer_core/rendermodes.py b/overviewer_core/rendermodes.py index e1adc9c..d854d0f 100644 --- a/overviewer_core/rendermodes.py +++ b/overviewer_core/rendermodes.py @@ -52,6 +52,10 @@ class Nether(RenderPrimitive): class HeightFading(RenderPrimitive): name = "height-fading" + options = { + # 128 is *WRONG*, it should be 64. but we're grandfathered in for now + "sealevel": ("target sea level", 128), + } black_color = Image.new("RGB", (24,24), (0,0,0)) white_color = Image.new("RGB", (24,24), (255,255,255)) @@ -62,6 +66,15 @@ class Depth(RenderPrimitive): "min": ("lowest level of blocks to render", 0), "max": ("highest level of blocks to render", 255), } + +class Exposed(RenderPrimitive): + name = "exposed" + options = { + "mode": ("0 = exposed blocks only, 1 = unexposed blocks only", 0), + } + +class NoFluids(RenderPrimitive): + name = "no-fluids" class EdgeLines(RenderPrimitive): name = "edge-lines" @@ -182,12 +195,21 @@ class Overlay(RenderPrimitive): class SpawnOverlay(Overlay): name = "overlay-spawn" +class SlimeOverlay(Overlay): + name = "overlay-slime" + class MineralOverlay(Overlay): name = "overlay-mineral" options = { 'minerals' : ('a list of (blockid, (r, g, b)) tuples for coloring minerals', None), } +class Hide(RenderPrimitive): + name = "hide" + options = { + 'blocks' : ('a list of blockids or (blockid, data) tuples of blocks to hide', []), + } + # Built-in rendermodes for your convenience! normal = [Base(), EdgeLines()] lighting = [Base(), EdgeLines(), Lighting()] diff --git a/overviewer_core/settingsDefinition.py b/overviewer_core/settingsDefinition.py index cfb6539..c422672 100644 --- a/overviewer_core/settingsDefinition.py +++ b/overviewer_core/settingsDefinition.py @@ -75,11 +75,13 @@ renders = Setting(required=True, default=util.OrderedDict(), "nomarkers": Setting(required=False, validator=validateBool, default=None), "texturepath": Setting(required=False, validator=validateTexturePath, default=None), "renderchecks": Setting(required=False, validator=validateInt, default=None), - "rerenderprob": Setting(required=True, validator=validateFloat, default=0), + "rerenderprob": Setting(required=True, validator=validateRerenderprob, default=0), "crop": Setting(required=False, validator=validateCrop, default=None), "changelist": Setting(required=False, validator=validateStr, default=None), "markers": Setting(required=False, validator=validateMarkers, default=[]), + "overlay": Setting(required=False, validator=validateOverlays, default=[]), "showspawn": Setting(required=False, validator=validateBool, default=True), + "base": Setting(required=False, validator=validateStr, default=""), # Remove this eventually (once people update their configs) "worldname": Setting(required=False, default=None, @@ -98,9 +100,10 @@ processes = Setting(required=True, validator=int, default=-1) # ends up adding overhead and isn't worth it. memcached_host = Setting(required=False, validator=str, default=None) -if platform.system() == 'Windows' or not sys.stderr.isatty(): +# TODO clean up this ugly in sys.argv hack +if platform.system() == 'Windows' or not sys.stdout.isatty() or "--simple" in sys.argv: obs = LoggingObserver() else: - obs = ProgressBarObserver() + obs = ProgressBarObserver(fd=sys.stdout) observer = Setting(required=True, validator=validateObserver, default=obs) diff --git a/overviewer_core/settingsValidators.py b/overviewer_core/settingsValidators.py index d8846b1..58e4835 100644 --- a/overviewer_core/settingsValidators.py +++ b/overviewer_core/settingsValidators.py @@ -17,6 +17,12 @@ class Setting(object): self.validator = validator self.default = default +def expand_path(p): + p = os.path.expanduser(p) + p = os.path.expandvars(p) + p = os.path.abspath(p) + return p + def checkBadEscape(s): fixed = False fixed_string = s @@ -45,15 +51,29 @@ def checkBadEscape(s): def validateMarkers(filterlist): if type(filterlist) != list: - raise ValidationException("Markers must specify a list of filters") + raise ValidationException("Markers must specify a list of filters. This has recently changed, so check the docs.") for x in filterlist: - if not callable(x): - raise ValidationException("%r must be a function"% x) + if type(x) != dict: + raise ValidationException("Markers must specify a list of dictionaries. This has recently changed, so check the docs.") + if "name" not in x: + raise ValidationException("Must define a name") + if "filterFunction" not in x: + raise ValidationException("Must define a filter function") + if not callable(x['filterFunction']): + raise ValidationException("%r must be a function"% x['filterFunction']) return filterlist +def validateOverlays(renderlist): + if type(renderlist) != list: + raise ValidationException("Overlay must specify a list of renders") + for x in renderlist: + if validateStr(x) == '': + raise ValidationException("%r must be a string"% x) + return renderlist + def validateWorldPath(worldpath): _, worldpath = checkBadEscape(worldpath) - abs_path = os.path.abspath(os.path.expanduser(worldpath)) + abs_path = expand_path(worldpath) if not os.path.exists(os.path.join(abs_path, "level.dat")): raise ValidationException("No level.dat file in '%s'. Are you sure you have the right path?" % (abs_path,)) return abs_path @@ -99,10 +119,10 @@ def validateNorthDirection(direction): raise ValidationException("%r is not a valid north direction" % direction) return intdir -def validateStochastic(s): +def validateRerenderprob(s): val = float(s) - if val < 0 or val > 1: - raise ValidationException("%r is not a valid stochastic value. Should be between 0.0 and 1.0" % s) + if val < 0 or val >= 1: + raise ValidationException("%r is not a valid rerender probability value. Should be between 0.0 and 1.0." % s) return val def validateImgFormat(fmt): @@ -144,7 +164,7 @@ def validateOptImg(opt): def validateTexturePath(path): # Expand user dir in directories strings - path = os.path.expanduser(path) + path = expand_path(path) # TODO assert this path exists? return path @@ -170,7 +190,7 @@ def validateOutputDir(d): _, d = checkBadEscape(d) if not d.strip(): raise ValidationException("You must specify a valid output directory") - return os.path.abspath(d) + return expand_path(d) def validateCrop(value): if len(value) != 4: diff --git a/overviewer_core/src/composite.c b/overviewer_core/src/composite.c index 2fc1c73..3267740 100644 --- a/overviewer_core/src/composite.c +++ b/overviewer_core/src/composite.c @@ -24,11 +24,6 @@ #include "overviewer.h" -/* like (a * b + 127) / 255), but much faster on most platforms - from PIL's _imaging.c */ -#define MULDIV255(a, b, tmp) \ - (tmp = (a) * (b) + 128, ((((tmp) >> 8) + (tmp)) >> 8)) - typedef struct { PyObject_HEAD Imaging image; diff --git a/overviewer_core/src/iterate.c b/overviewer_core/src/iterate.c index bbc9446..6411f90 100644 --- a/overviewer_core/src/iterate.c +++ b/overviewer_core/src/iterate.c @@ -432,7 +432,6 @@ generate_pseudo_data(RenderState *state, unsigned char ancilData) { PyObject* chunk_render(PyObject *self, PyObject *args) { RenderState state; - PyObject *regionset; PyObject *modeobj; PyObject *blockmap; @@ -453,7 +452,7 @@ chunk_render(PyObject *self, PyObject *args) { PyObject *t = NULL; - if (!PyArg_ParseTuple(args, "OiiiOiiOO", &state.regionset, &state.chunkx, &state.chunky, &state.chunkz, &state.img, &xoff, &yoff, &modeobj, &state.textures)) + if (!PyArg_ParseTuple(args, "OOiiiOiiOO", &state.world, &state.regionset, &state.chunkx, &state.chunky, &state.chunkz, &state.img, &xoff, &yoff, &modeobj, &state.textures)) return NULL; /* set up the render mode */ diff --git a/overviewer_core/src/overviewer.h b/overviewer_core/src/overviewer.h index 1bb8f92..97a5a2e 100644 --- a/overviewer_core/src/overviewer.h +++ b/overviewer_core/src/overviewer.h @@ -26,13 +26,18 @@ // increment this value if you've made a change to the c extesion // and want to force users to rebuild -#define OVERVIEWER_EXTENSION_VERSION 30 +#define OVERVIEWER_EXTENSION_VERSION 37 /* Python PIL, and numpy headers */ #include #include #include +/* like (a * b + 127) / 255), but much faster on most platforms + from PIL's _imaging.c */ +#define MULDIV255(a, b, tmp) \ + (tmp = (a) * (b) + 128, ((((tmp) >> 8) + (tmp)) >> 8)) + /* macro for getting a value out of various numpy arrays the 3D arrays have interesting, swizzled coordinates because minecraft (anvil) stores blocks in y/z/x order for 3D, z/x order for 2D */ @@ -83,6 +88,7 @@ typedef struct { } ChunkData; typedef struct { /* the regionset object, and chunk coords */ + PyObject *world; PyObject *regionset; int chunkx, chunky, chunkz; diff --git a/overviewer_core/src/primitives/base.c b/overviewer_core/src/primitives/base.c index 5a66a9a..64f0443 100644 --- a/overviewer_core/src/primitives/base.c +++ b/overviewer_core/src/primitives/base.c @@ -29,46 +29,53 @@ typedef struct { typedef struct { const char* name; + float temperature; float rainfall; + + unsigned int r, g, b; } Biome; /* each entry in this table is yanked *directly* out of the minecraft source * temp/rainfall are taken from what MCP calls setTemperatureRainfall * + * Some biomes, like Swamp, do a bit of post-processing by multiplying on a + * hard-coded color. The RGB tuple used follows the temp/rainfall. + * 255, 255, 255 is white, which means do nothing + * * keep in mind the x/y coordinate in the color tables is found *after* * multiplying rainfall and temperature for the second coordinate, *and* the * origin is in the lower-right. <3 biomes. */ static Biome biome_table[] = { /* 0 */ - {"Ocean", 0.5, 0.5}, - {"Plains", 0.8, 0.4}, - {"Desert", 2.0, 0.0}, - {"Extreme Hills", 0.2, 0.3}, - {"Forest", 0.7, 0.8}, + {"Ocean", 0.5, 0.5, 255, 255, 255}, + {"Plains", 0.8, 0.4, 255, 255, 255}, + {"Desert", 2.0, 0.0, 255, 255, 255}, + {"Extreme Hills", 0.2, 0.3, 255, 255, 255}, + {"Forest", 0.7, 0.8, 255, 255, 255}, /* 5 */ - {"Taiga", 0.05, 0.8}, - {"Swampland", 0.8, 0.9}, - {"River", 0.5, 0.5}, - {"Hell", 2.0, 0.0}, - {"Sky", 0.5, 0.5}, + {"Taiga", 0.05, 0.8, 255, 255, 255}, + {"Swampland", 0.8, 0.9, 205, 128, 255}, + {"River", 0.5, 0.5, 255, 255, 255}, + {"Hell", 2.0, 0.0, 255, 255, 255}, + {"Sky", 0.5, 0.5, 255, 255, 255}, /* 10 */ - {"FrozenOcean", 0.0, 0.5}, - {"FrozenRiver", 0.0, 0.5}, - {"Ice Plains", 0.0, 0.5}, - {"Ice Mountains", 0.0, 0.5}, - {"MushroomIsland", 0.9, 1.0}, + {"FrozenOcean", 0.0, 0.5, 255, 255, 255}, + {"FrozenRiver", 0.0, 0.5, 255, 255, 255}, + {"Ice Plains", 0.0, 0.5, 255, 255, 255}, + {"Ice Mountains", 0.0, 0.5, 255, 255, 255}, + {"MushroomIsland", 0.9, 1.0, 255, 255, 255}, /* 15 */ - {"MushroomIslandShore", 0.9, 1.0}, - {"Beach", 0.8, 0.4}, - {"DesertHills", 2.0, 0.0}, - {"ForestHills", 0.7, 0.8}, - {"TaigaHills", 0.05, 0.8}, + {"MushroomIslandShore", 0.9, 1.0, 255, 255, 255}, + {"Beach", 0.8, 0.4, 255, 255, 255}, + {"DesertHills", 2.0, 0.0, 255, 255, 255}, + {"ForestHills", 0.7, 0.8, 255, 255, 255}, + {"TaigaHills", 0.05, 0.8, 255, 255, 255}, /* 20 */ - {"Extreme Hills Edge", 0.2, 0.3}, - {"Jungle", 2.0, 0.45}, /* <-- GUESS, but a good one */ - {"Jungle Mountains", 2.0, 0.45}, /* <-- also a guess */ + {"Extreme Hills Edge", 0.2, 0.3, 255, 255, 255}, + {"Jungle", 2.0, 0.45, 255, 255, 255}, /* <-- GUESS, but a good one */ + {"Jungle Mountains", 2.0, 0.45, 255, 255, 255}, /* <-- also a guess */ }; #define NUM_BIOMES (sizeof(biome_table) / sizeof(Biome)) @@ -206,16 +213,19 @@ base_draw(void *data, RenderState *state, PyObject *src, PyObject *mask, PyObjec }; if (color_table) { + unsigned char biome; int dx, dz; unsigned char tablex, tabley; float temp = 0.0, rain = 0.0; + unsigned int multr = 0, multg = 0, multb = 0; + int tmp; PyObject *color = NULL; if (self->use_biomes) { /* average over all neighbors */ for (dx = -1; dx <= 1; dx++) { for (dz = -1; dz <= 1; dz++) { - unsigned char biome = get_data(state, BIOMES, state->x + dx, state->y, state->z + dz); + biome = get_data(state, BIOMES, state->x + dx, state->y, state->z + dz); if (biome >= NUM_BIOMES) { /* note -- biome 255 shows up on map borders. who knows what it is? certainly not I. @@ -225,14 +235,24 @@ base_draw(void *data, RenderState *state, PyObject *src, PyObject *mask, PyObjec temp += biome_table[biome].temperature; rain += biome_table[biome].rainfall; + multr += biome_table[biome].r; + multg += biome_table[biome].g; + multb += biome_table[biome].b; } } + temp /= 9.0; rain /= 9.0; + multr /= 9; + multg /= 9; + multb /= 9; } else { /* don't use biomes, just use the default */ temp = biome_table[DEFAULT_BIOME].temperature; rain = biome_table[DEFAULT_BIOME].rainfall; + multr = biome_table[DEFAULT_BIOME].r; + multg = biome_table[DEFAULT_BIOME].g; + multb = biome_table[DEFAULT_BIOME].b; } /* second coordinate is actually scaled to fit inside the triangle @@ -258,6 +278,11 @@ base_draw(void *data, RenderState *state, PyObject *src, PyObject *mask, PyObjec g = PyInt_AsLong(PyTuple_GET_ITEM(color, 1)); b = PyInt_AsLong(PyTuple_GET_ITEM(color, 2)); Py_DECREF(color); + + /* do the after-coloration */ + r = MULDIV255(r, multr, tmp); + g = MULDIV255(g, multg, tmp); + b = MULDIV255(b, multb, tmp); } /* final coloration */ diff --git a/overviewer_core/src/primitives/edge-lines.c b/overviewer_core/src/primitives/edge-lines.c index dc3d1c1..6114309 100644 --- a/overviewer_core/src/primitives/edge-lines.c +++ b/overviewer_core/src/primitives/edge-lines.c @@ -38,23 +38,24 @@ edge_lines_draw(void *data, RenderState *state, PyObject *src, PyObject *mask, P Imaging img_i = imaging_python_to_c(state->img); unsigned char ink[] = {0, 0, 0, 255 * self->opacity}; unsigned short side_block; + int x = state->x, y = state->y, z = state->z; int increment=0; - if (state->block == 44 && ((state->block_data & 0x8) == 0 )) // half-step BUT no upsidown half-step + if ((state->block == 44 || state->block == 126) && ((state->block_data & 0x8) == 0 )) // half-steps BUT no upsidown half-steps increment=6; else if ((state->block == 78) || (state->block == 93) || (state->block == 94)) // snow, redstone repeaters (on and off) increment=9; /* +X side */ - side_block = get_data(state, BLOCKS, state->x+1, state->y, state->z); - if (side_block != state->block && is_transparent(side_block)) { + side_block = get_data(state, BLOCKS, x+1, y, z); + if (side_block != state->block && (is_transparent(side_block) || render_mode_hidden(state->rendermode, x+1, y, z))) { ImagingDrawLine(img_i, state->imgx+12, state->imgy+1+increment, state->imgx+22+1, state->imgy+5+1+increment, &ink, 1); ImagingDrawLine(img_i, state->imgx+12, state->imgy+increment, state->imgx+22+1, state->imgy+5+increment, &ink, 1); } /* -Z side */ - side_block = get_data(state, BLOCKS, state->x, state->y, state->z-1); - if (side_block != state->block && is_transparent(side_block)) { + side_block = get_data(state, BLOCKS, x, y, z-1); + if (side_block != state->block && (is_transparent(side_block) || render_mode_hidden(state->rendermode, x, y, z-1))) { ImagingDrawLine(img_i, state->imgx, state->imgy+6+1+increment, state->imgx+12+1, state->imgy+1+increment, &ink, 1); ImagingDrawLine(img_i, state->imgx, state->imgy+6+increment, state->imgx+12+1, state->imgy+increment, &ink, 1); } diff --git a/overviewer_core/src/primitives/exposed.c b/overviewer_core/src/primitives/exposed.c new file mode 100644 index 0000000..1047beb --- /dev/null +++ b/overviewer_core/src/primitives/exposed.c @@ -0,0 +1,109 @@ +/* + * This file is part of the Minecraft Overviewer. + * + * Minecraft Overviewer is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as published + * by the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Minecraft Overviewer is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with the Overviewer. If not, see . + */ + +#include "../overviewer.h" + +typedef struct { + unsigned int mode; /* 0 = exposed only, 1 = unexposed only */ +} PrimitiveExposed; + +static int +exposed_start(void *data, RenderState *state, PyObject *support) { + PrimitiveExposed *self = (PrimitiveExposed *)data; + + if (!render_mode_parse_option(support, "mode", "I", &(self->mode))) + return 1; + + return 0; +} + +static int +exposed_hidden(void *data, RenderState *state, int x, int y, int z) { + PrimitiveExposed *self = (PrimitiveExposed *)data; + + /* Unset these flags if seeming exposure from any of these directions would + * be due to not having data there. + */ + int validMinusX = 1; + int validPlusX = 1; + int validMinusY = 1; + int validPlusY = 1; + int validMinusZ = 1; + int validPlusZ = 1; + + /* special handling for section boundaries */ + /* If the neighboring section has no block data, ignore exposure from that + * direction + */ + if (x == 0 && (!(state->chunks[0][1].loaded) || state->chunks[0][1].sections[state->chunky].blocks == NULL)) { + /* No data in -x direction */ + validMinusX = 0; + } + + if (x == 15 && (!(state->chunks[2][1].loaded) || state->chunks[2][1].sections[state->chunky].blocks == NULL)) { + /* No data in +x direction */ + validPlusX = 0; + } + + if (y == 0 && (state->chunky - 1 < 0 || state->chunks[1][1].sections[state->chunky - 1].blocks == NULL)) { + /* No data in -y direction */ + validMinusY = 0; + } + + if (y == 15 && (state->chunky + 1 >= SECTIONS_PER_CHUNK || state->chunks[1][1].sections[state->chunky + 1].blocks == NULL)) { + /* No data in +y direction */ + validPlusY = 0; + } + + if (z == 0 && (!(state->chunks[1][0].loaded) || state->chunks[1][0].sections[state->chunky].blocks == NULL)) { + /* No data in -z direction */ + validMinusZ = 0; + } + + if (z == 15 && (!(state->chunks[1][2].loaded) || state->chunks[1][2].sections[state->chunky].blocks == NULL)) { + /* No data in +z direction */ + validPlusZ = 0; + } + + /* If any of the 6 blocks adjacent to us are transparent, we're exposed */ + if( (validMinusX && is_transparent(get_data(state, BLOCKS, x-1, y, z))) || + (validPlusX && is_transparent(get_data(state, BLOCKS, x+1, y, z))) || + (validMinusY && is_transparent(get_data(state, BLOCKS, x, y-1, z))) || + (validPlusY && is_transparent(get_data(state, BLOCKS, x, y+1, z))) || + (validMinusZ && is_transparent(get_data(state, BLOCKS, x, y, z-1))) || + (validPlusZ && is_transparent(get_data(state, BLOCKS, x, y, z+1 ))) ) { + + /* Block is exposed */ + /* Returns 1 and hides us if we're rendering unexposed blocks, 0 and + * shows us if we're rendering exposed blocks + */ + return self->mode; + + } + + /* We have no valid evidence that the block is exposed */ + return !(self->mode); /* Hide in normal mode, reveal in inverted mode */ +} + +RenderPrimitiveInterface primitive_exposed = { + "exposed", sizeof(PrimitiveExposed), + exposed_start, + NULL, + NULL, + exposed_hidden, + NULL, +}; diff --git a/overviewer_core/src/primitives/height-fading.c b/overviewer_core/src/primitives/height-fading.c index ae7f85b..e177492 100644 --- a/overviewer_core/src/primitives/height-fading.c +++ b/overviewer_core/src/primitives/height-fading.c @@ -20,12 +20,16 @@ typedef struct { PyObject *black_color; PyObject *white_color; + unsigned int sealevel; } PrimitiveHeightFading; static int height_fading_start(void *data, RenderState *state, PyObject *support) { PrimitiveHeightFading *self = (PrimitiveHeightFading *)data; + if (!render_mode_parse_option(support, "sealevel", "I", &(self->sealevel))) + return 1; + self->black_color = PyObject_GetAttrString(support, "black_color"); self->white_color = PyObject_GetAttrString(support, "white_color"); @@ -50,7 +54,7 @@ height_fading_draw(void *data, RenderState *state, PyObject *src, PyObject *mask PyObject *height_color = self->white_color; /* current formula requires y to be between 0 and 127, so scale it */ - y = (y * 128) / (16 * SECTIONS_PER_CHUNK); + y = (y * 128) / (2 * self->sealevel); /* negative alpha => darkness, positive => light */ alpha = (1.0 / (1 + expf((70 - y) / 11.0))) * 0.6 - 0.55; diff --git a/overviewer_core/src/primitives/hide.c b/overviewer_core/src/primitives/hide.c new file mode 100644 index 0000000..cdb24e8 --- /dev/null +++ b/overviewer_core/src/primitives/hide.c @@ -0,0 +1,117 @@ +/* + * This file is part of the Minecraft Overviewer. + * + * Minecraft Overviewer is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as published + * by the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Minecraft Overviewer is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with the Overviewer. If not, see . + */ + +#include "../overviewer.h" + +struct HideRule { + unsigned short blockid; + unsigned char has_data; + unsigned char data; +}; + +typedef struct { + struct HideRule* rules; +} RenderPrimitiveHide; + +static int +hide_start(void *data, RenderState *state, PyObject *support) { + PyObject *opt; + RenderPrimitiveHide* self = (RenderPrimitiveHide *)data; + self->rules = NULL; + + if (!render_mode_parse_option(support, "blocks", "O", &(opt))) + return 1; + if (opt && opt != Py_None) { + Py_ssize_t blocks_size = 0, i; + + if (!PyList_Check(opt)) { + PyErr_SetString(PyExc_TypeError, "'blocks' must be a list"); + return 1; + } + + blocks_size = PyList_GET_SIZE(opt); + self->rules = calloc(blocks_size + 1, sizeof(struct HideRule)); + if (self->rules == NULL) { + return 1; + } + + for (i = 0; i < blocks_size; i++) { + PyObject *block = PyList_GET_ITEM(opt, i); + + if (PyInt_Check(block)) { + /* format 1: just a block id */ + self->rules[i].blockid = PyInt_AsLong(block); + self->rules[i].has_data = 0; + } else if (PyArg_ParseTuple(block, "Hb", &(self->rules[i].blockid), &(self->rules[i].data))) { + /* format 2: (blockid, data) */ + self->rules[i].has_data = 1; + } else { + /* format not recognized */ + free(self->rules); + self->rules = NULL; + return 1; + } + } + } + + return 0; +} + +static void +hide_finish(void *data, RenderState *state) { + RenderPrimitiveHide *self = (RenderPrimitiveHide *)data; + + if (self->rules) { + free(self->rules); + } +} + +static int +hide_hidden(void *data, RenderState *state, int x, int y, int z) { + RenderPrimitiveHide *self = (RenderPrimitiveHide *)data; + unsigned int i; + unsigned short block; + + if (self->rules == NULL) + return 0; + + block = get_data(state, BLOCKS, x, y, z); + for (i = 0; self->rules[i].blockid != 0; i++) { + if (block == self->rules[i].blockid) { + unsigned char data; + + if (!(self->rules[i].has_data)) + return 1; + + data = get_data(state, DATA, x, y, z); + if (data == self->rules[i].data) + return 1; + } + } + + return 0; +} + +RenderPrimitiveInterface primitive_hide = { + "hide", + sizeof(RenderPrimitiveHide), + hide_start, + hide_finish, + NULL, + hide_hidden, + NULL, +}; diff --git a/overviewer_core/src/primitives/lighting.c b/overviewer_core/src/primitives/lighting.c index 79895a4..317ac98 100644 --- a/overviewer_core/src/primitives/lighting.c +++ b/overviewer_core/src/primitives/lighting.c @@ -202,7 +202,7 @@ lighting_is_face_occluded(RenderState *state, int skip_sides, int x, int y, int /* this face isn't visible, so don't draw anything */ return 1; } - } else if (skip_sides) { + } else if (!skip_sides) { unsigned short block = get_data(state, BLOCKS, x, y, z); if (!is_transparent(block)) { /* the same thing but for adjacent chunks, this solves an @@ -242,8 +242,8 @@ lighting_start(void *data, RenderState *state, PyObject *support) { RenderPrimitiveLighting* self; self = (RenderPrimitiveLighting *)data; - /* skip sides by default */ - self->skip_sides = 1; + /* don't skip sides by default */ + self->skip_sides = 0; if (!render_mode_parse_option(support, "strength", "f", &(self->strength))) return 1; diff --git a/overviewer_core/src/primitives/no-fluids.c b/overviewer_core/src/primitives/no-fluids.c new file mode 100644 index 0000000..cb6428a --- /dev/null +++ b/overviewer_core/src/primitives/no-fluids.c @@ -0,0 +1,37 @@ +/* + * This file is part of the Minecraft Overviewer. + * + * Minecraft Overviewer is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as published + * by the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Minecraft Overviewer is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with the Overviewer. If not, see . + */ + +#include "../overviewer.h" + +static int +no_fluids_start(void *data, RenderState *state, PyObject *support) { + return 0; +} + +static int +no_fluids_hidden(void *data, RenderState *state, int x, int y, int z) { + return block_has_property(state->block, FLUID); +} + +RenderPrimitiveInterface primitive_no_fluids = { + "no-fluids", 0, + no_fluids_start, + NULL, + NULL, + no_fluids_hidden, + NULL, +}; diff --git a/overviewer_core/src/primitives/overlay-mineral.c b/overviewer_core/src/primitives/overlay-mineral.c index 5d6f22e..b658433 100644 --- a/overviewer_core/src/primitives/overlay-mineral.c +++ b/overviewer_core/src/primitives/overlay-mineral.c @@ -90,6 +90,7 @@ overlay_mineral_start(void *data, RenderState *state, PyObject *support) { /* now do custom initializations */ self = (RenderPrimitiveMineral *)data; + // opt is a borrowed reference. do not deref if (!render_mode_parse_option(support, "minerals", "O", &(opt))) return 1; if (opt && opt != Py_None) { @@ -119,7 +120,6 @@ overlay_mineral_start(void *data, RenderState *state, PyObject *support) { } else { self->minerals = default_minerals; } - Py_XDECREF(opt); /* setup custom color */ self->parent.get_color = get_color; diff --git a/overviewer_core/src/primitives/overlay-slime.c b/overviewer_core/src/primitives/overlay-slime.c new file mode 100644 index 0000000..f4abfe8 --- /dev/null +++ b/overviewer_core/src/primitives/overlay-slime.c @@ -0,0 +1,126 @@ +/* + * This file is part of the Minecraft Overviewer. + * + * Minecraft Overviewer is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as published + * by the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Minecraft Overviewer is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with the Overviewer. If not, see . + */ + +#include "overlay.h" +#include + +typedef struct { + /* inherits from overlay */ + RenderPrimitiveOverlay parent; + long seed; +} RenderPrimitiveSlime; + +/* + * random_* are a re-implementation of java's Random() class + * since Minecraft's slime algorithm depends on it + * http://docs.oracle.com/javase/1.4.2/docs/api/java/util/Random.html + */ + +static void random_set_seed(long *seed, long new_seed) { + *seed = (new_seed ^ 0x5deece66dL) & ((1L << 48) - 1); +} + +static int random_next(long *seed, int bits) { + *seed = (*seed * 0x5deece66dL + 0xbL) & ((1L << 48) - 1); + return (int)(*seed >> (48 - bits)); +} + +static int random_next_int(long *seed, int n) { + int bits, val; + + if (n <= 0) { + /* invalid */ + return 0; + } + + if ((n & -n) == n) { + /* n is a power of two */ + return (int)((n * (long)random_next(seed, 31)) >> 31); + } + + do { + bits = random_next(seed, 31); + val = bits % n; + } while (bits - val + (n - 1) < 0); + return val; +} + +static int is_slime(long map_seed, long chunkx, long chunkz) { + /* lots of magic numbers, but they're all correct! I swear! */ + long seed; + random_set_seed(&seed, map_seed + (chunkx * chunkx * 0x4c1906L) + (chunkx * 0x5ac0dbL) + (chunkz * chunkz * 0x4307a7L) + (chunkz * 0x5f24fL) ^ 0x3ad8025fL); + return (random_next_int(&seed, 10) == 0); +} + +static void get_color(void *data, RenderState *state, + unsigned char *r, unsigned char *g, unsigned char *b, unsigned char *a) { + RenderPrimitiveSlime *self = (RenderPrimitiveSlime *)data; + + /* set a nice, pretty green color */ + *r = 40; + *g = 230; + *b = 40; + + /* default to no overlay, until told otherwise */ + *a = 0; + + if (is_slime(self->seed, state->chunkx, state->chunkz)) { + /* slimes can spawn! */ + *a = 240; + } +} + +static int +overlay_slime_start(void *data, RenderState *state, PyObject *support) { + RenderPrimitiveSlime *self; + PyObject *pyseed; + + /* first, chain up */ + int ret = primitive_overlay.start(data, state, support); + if (ret != 0) + return ret; + + /* now do custom initializations */ + self = (RenderPrimitiveSlime *)data; + self->parent.get_color = get_color; + + pyseed = PyObject_GetAttrString(state->world, "seed"); + if (!pyseed) + return 1; + self->seed = PyInt_AsLong(pyseed); + Py_DECREF(pyseed); + if (PyErr_Occurred()) + return 1; + + return 0; +} + +static void +overlay_slime_finish(void *data, RenderState *state) { + /* chain up */ + primitive_overlay.finish(data, state); +} + +RenderPrimitiveInterface primitive_overlay_slime = { + "overlay-slime", + sizeof(RenderPrimitiveSlime), + overlay_slime_start, + overlay_slime_finish, + NULL, + NULL, + overlay_draw, +}; diff --git a/overviewer_core/src/primitives/overlay.c b/overviewer_core/src/primitives/overlay.c index 0645a4d..1e02489 100644 --- a/overviewer_core/src/primitives/overlay.c +++ b/overviewer_core/src/primitives/overlay.c @@ -82,8 +82,8 @@ overlay_draw(void *data, RenderState *state, PyObject *src, PyObject *mask, PyOb /* do the overlay */ if (a > 0) { - alpha_over(state->img, self->white_color, self->facemask_top, state->imgx, state->imgy + increment, 0, 0); - tint_with_mask(state->img, r, g, b, a, self->facemask_top, state->imgx, state->imgy + increment, 0, 0); + alpha_over_full(state->img, self->white_color, self->facemask_top, a/255.f, state->imgx, state->imgy + increment, 0, 0); + tint_with_mask(state->img, r, g, b, 255, self->facemask_top, state->imgx, state->imgy + increment, 0, 0); } } diff --git a/overviewer_core/textures.py b/overviewer_core/textures.py index ca891de..c150101 100644 --- a/overviewer_core/textures.py +++ b/overviewer_core/textures.py @@ -213,7 +213,7 @@ class Textures(object): if verbose: logging.info("Found %s in '%s'", filename, path) return open(path, mode) - raise IOError("Could not find the file `{0}'. Try specifying the 'texturepath' option in your config file. Set it to the directory where I can find {0}.".format(filename)) + raise IOError("Could not find the file `{0}'. Try specifying the 'texturepath' option in your config file. Set it to the directory where I can find {0}. Also see ".format(filename)) def load_image(self, filename): """Returns an image object""" @@ -1336,6 +1336,8 @@ block(blockid=41, top_index=23) block(blockid=42, top_index=22) # double slabs and slabs +# these wooden slabs are unobtainable without cheating, they are still +# here because lots of pre-1.3 worlds use this blocks @material(blockid=[43, 44], data=range(16), transparent=(44,), solid=True) def slabs(self, blockid, data): texture = data & 7 @@ -3169,3 +3171,52 @@ block(blockid=123, top_index=211) # active redstone lamp block(blockid=124, top_index=212) + +# wooden double and normal slabs +# these are the new wooden slabs, blockids 43 44 still have wooden +# slabs, but those are unobtainable without cheating +@material(blockid=[125, 126], data=range(16), transparent=(44,), solid=True) +def slabs(self, blockid, data): + texture = data & 7 + if texture== 0: # oak + top = side = self.terrain_images[4] + elif texture== 1: # spruce + top = side = self.terrain_images[198] + elif texture== 2: # birch + top = side = self.terrain_images[214] + elif texture== 3: # jungle + top = side = self.terrain_images[199] + else: + return None + + if blockid == 125: # double slab + return self.build_block(top, side) + + # cut the side texture in half + mask = side.crop((0,8,16,16)) + side = Image.new(side.mode, side.size, self.bgcolor) + alpha_over(side, mask,(0,0,16,8), mask) + + # plain slab + top = self.transform_image_top(top) + side = self.transform_image_side(side) + otherside = side.transpose(Image.FLIP_LEFT_RIGHT) + + sidealpha = side.split()[3] + side = ImageEnhance.Brightness(side).enhance(0.9) + side.putalpha(sidealpha) + othersidealpha = otherside.split()[3] + otherside = ImageEnhance.Brightness(otherside).enhance(0.8) + otherside.putalpha(othersidealpha) + + # upside down slab + delta = 0 + if data & 8 == 8: + delta = 6 + + img = Image.new("RGBA", (24,24), self.bgcolor) + alpha_over(img, side, (0,12 - delta), side) + alpha_over(img, otherside, (12,12 - delta), otherside) + alpha_over(img, top, (0,6 - delta), top) + + return img diff --git a/overviewer_core/tileset.py b/overviewer_core/tileset.py index ab25a2a..c2a8bec 100644 --- a/overviewer_core/tileset.py +++ b/overviewer_core/tileset.py @@ -17,6 +17,7 @@ import itertools import logging import os import os.path +import sys import shutil import random import functools @@ -32,6 +33,7 @@ from .util import roundrobin from . import nbt from .files import FileReplacer from .optimizeimages import optimize_image +import rendermodes import c_overviewer """ @@ -163,12 +165,14 @@ class TileSet(object): """ - def __init__(self, regionsetobj, assetmanagerobj, texturesobj, options, outputdir): + def __init__(self, worldobj, regionsetobj, assetmanagerobj, texturesobj, options, outputdir): """Construct a new TileSet object with the given configuration options dictionary. options is a dictionary of configuration parameters (strings mapping to values) that are interpreted by the rendering engine. + + worldobj is the World object that regionsetobj is from. regionsetobj is the RegionSet object that is used to render the tiles. @@ -268,6 +272,7 @@ class TileSet(object): """ self.options = options + self.world = worldobj self.regionset = regionsetobj self.am = assetmanagerobj self.textures = texturesobj @@ -355,7 +360,7 @@ class TileSet(object): # Only pickle the initial state. Don't pickle anything resulting from the # do_preprocessing step def __getstate__(self): - return self.regionset, self.am, self.textures, self.options, self.outputdir + return self.world, self.regionset, self.am, self.textures, self.options, self.outputdir def __setstate__(self, state): self.__init__(*state) @@ -507,19 +512,25 @@ class TileSet(object): """ def bgcolorformat(color): return "#%02x%02x%02x" % color[0:3] + isOverlay = not any(isinstance(x, rendermodes.Base) for x in self.options.get("rendermode")) + d = dict(name = self.options.get('title'), zoomLevels = self.treedepth, minZoom = 0, defaultZoom = 1, maxZoom = self.treedepth, path = self.options.get('name'), - base = '', + base = self.options.get('base'), bgcolor = bgcolorformat(self.options.get('bgcolor')), world = self.options.get('worldname_orig') + (" - " + self.options.get('dimension') if self.options.get('dimension') != 'default' else ''), last_rendertime = self.max_chunk_mtime, imgextension = self.imgextension, + isOverlay = isOverlay ) + if isOverlay: + d.update({"tilesets": self.options.get("overlay")}) + if (self.regionset.get_type() == "overworld" and self.options.get("showspawn", True)): d.update({"spawn": self.options.get("spawn")}) else: @@ -580,7 +591,6 @@ class TileSet(object): self.xradius = xradius self.yradius = yradius - def _rearrange_tiles(self): """If the target size of the tree is not the same as the existing size on disk, do some re-arranging @@ -940,7 +950,7 @@ class TileSet(object): # draw the chunk! try: - c_overviewer.render_loop(self.regionset, chunkx, chunky, + c_overviewer.render_loop(self.world, self.regionset, chunkx, chunky, chunkz, tileimg, xpos, ypos, self.options['rendermode'], self.textures) except nbt.CorruptionError: @@ -948,8 +958,9 @@ class TileSet(object): # get_chunk() logging.debug("Skipping the render of corrupt chunk at %s,%s and moving on.", chunkx, chunkz) except Exception, e: - logging.warning("Could not render chunk %s,%s for some reason. I'm going to ignore this and continue", chunkx, chunkz) - logging.debug("Full error was:", exc_info=1) + logging.error("Could not render chunk %s,%s for some reason. This is likely a render primitive option error.", chunkx, chunkz) + logging.error("Full error was:", exc_info=1) + sys.exit(1) ## Semi-handy routine for debugging the drawing routine ## Draw the outline of the top of the chunk diff --git a/overviewer_core/world.py b/overviewer_core/world.py index de6fd39..af76a49 100644 --- a/overviewer_core/world.py +++ b/overviewer_core/world.py @@ -136,7 +136,12 @@ class World(object): except KeyError: # but very old ones might not? so we'll just go with the world dir name if they don't self.name = os.path.basename(os.path.realpath(self.worlddir)) - + + try: + # level.dat also has a RandomSeed attribute + self.seed = data['RandomSeed'] + except KeyError: + self.seed = 0 # oh well # TODO figure out where to handle regionlists @@ -204,6 +209,7 @@ class World(object): if section['Y'] == targetSection: blockArray = section['Blocks'] return blockArray[inChunkX, inChunkZ, y % 16] + return 0 @@ -304,7 +310,7 @@ class RegionSet(object): * For each chunk section: * The "Blocks" byte string is transformed into a 16x16x16 numpy array - * The AddBlocks array, if it exists, is bitshifted left 8 bits and + * The Add array, if it exists, is bitshifted left 8 bits and added into the Blocks array * The "SkyLight" byte string is transformed into a 16x16x128 numpy array @@ -380,11 +386,11 @@ class RegionSet(object): # Cast up to uint16, blocks can have up to 12 bits of data blocks = blocks.astype(numpy.uint16) blocks = blocks.reshape((16,16,16)) - if "AddBlocks" in section: + if "Add" in section: # This section has additional bits to tack on to the blocks - # array. AddBlocks is a packed array with 4 bits per slot, so + # array. Add is a packed array with 4 bits per slot, so # it needs expanding - additional = numpy.frombuffer(section['AddBlocks'], dtype=numpy.uint8) + additional = numpy.frombuffer(section['Add'], dtype=numpy.uint8) additional = additional.astype(numpy.uint16).reshape((16,16,8)) additional_expanded = numpy.empty((16,16,16), dtype=numpy.uint16) additional_expanded[:,:,::2] = (additional & 0x0F) << 8 @@ -392,7 +398,7 @@ class RegionSet(object): blocks += additional_expanded del additional del additional_expanded - del section['AddBlocks'] # Save some memory + del section['Add'] # Save some memory section['Blocks'] = blocks # Turn the skylight array into a 16x16x16 matrix. The array comes @@ -483,6 +489,8 @@ class RegionSet(object): p = f.split(".") x = int(p[1]) y = int(p[2]) + if abs(x) > 500000 or abs(y) > 500000: + logging.warning("Holy shit what is up with region file %s !?" % f) yield (x, y, path) class RegionSetWrapper(object): diff --git a/setup.py b/setup.py index 583358c..5acc9bc 100755 --- a/setup.py +++ b/setup.py @@ -108,13 +108,13 @@ if py2exe is not None: setup_kwargs['data_files'] += recursive_data_files('overviewer_core/data/web_assets', 'web_assets') setup_kwargs['data_files'] += recursive_data_files('overviewer_core/data/js_src', 'js_src') setup_kwargs['data_files'] += recursive_data_files('contrib', 'contrib') - setup_kwargs['data_files'] += [('', ['genPOI.py'])] setup_kwargs['zipfile'] = None if platform.system() == 'Windows' and '64bit' in platform.architecture(): b = 3 else: b = 1 - setup_kwargs['options']['py2exe'] = {'bundle_files' : b, 'excludes': 'Tkinter', 'includes':['fileinput', 'overviewer_core.items']} + setup_kwargs['options']['py2exe'] = {'bundle_files' : b, 'excludes': 'Tkinter', 'includes': + ['fileinput', 'overviewer_core.items', 'overviewer_core.aux_files.genPOI']} # # py2app options @@ -129,7 +129,7 @@ if py2app is not None: # script, package, and data # -setup_kwargs['packages'] = ['overviewer_core'] +setup_kwargs['packages'] = ['overviewer_core', 'overviewer_core/aux_files'] setup_kwargs['scripts'] = ['overviewer.py'] setup_kwargs['package_data'] = {'overviewer_core': recursive_package_data('data/textures') + recursive_package_data('data/web_assets') + recursive_package_data('data/js_src')}