diff --git a/docs/config.rst b/docs/config.rst index fe97059..06329a4 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -552,6 +552,18 @@ 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 renders. + + .. warning:: + + At this time, this feature is not fully implemented. + + **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. @@ -636,6 +648,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. diff --git a/docs/signs.rst b/docs/signs.rst index d10e755..415e581 100644 --- a/docs/signs.rst +++ b/docs/signs.rst @@ -20,51 +20,97 @@ 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")] } +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) Generating the POI Markers diff --git a/genPOI.py b/genPOI.py index 66a8360..9b80a60 100755 --- a/genPOI.py +++ b/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,43 @@ 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") + 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'])) except KeyError: - markers[rname] = [dict(groupName=name, displayName=f.__doc__),] + markers[rname] = [dict(groupName=name, displayName=f['name'], icon=d['icon']),] 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, createInfoWindow=True) + if "icon" in poi: + d.update({"icon": poi['icon']}) + 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, createInfoWindow=True) + if "icon" in poi: + d.update({"icon": poi['icon']}) + markerSetDict[name]['raw'].append(d) #print markerSetDict with open(os.path.join(destdir, "markersDB.js"), "w") as output: diff --git a/overviewer.py b/overviewer.py index c31b51c..67193b6 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", @@ -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,7 +421,7 @@ 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","base"]) + 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) tilesets.append(tset) 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/data/js_src/util.js b/overviewer_core/data/js_src/util.js index 0e738da..81cc5c0 100644 --- a/overviewer_core/data/js_src/util.js +++ b/overviewer_core/data/js_src/util.js @@ -63,6 +63,8 @@ overviewer.util = { signs.registerEvents(signs); } + var overlayControl = new overviewer.views.OverlayControlView(); + var spawnmarker = new overviewer.views.SpawnIconView(); // Update coords on mousemove @@ -81,6 +83,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) { @@ -113,8 +118,6 @@ overviewer.util = { }); - var worldSelector = new overviewer.views.WorldSelectorView({tagName:'DIV'}); - overviewer.collections.worlds.bind("add", worldSelector.render, worldSelector); // hook up some events @@ -125,6 +128,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 323c1fc..3ca20c4 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,23 @@ 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")); + + 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 +56,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 +65,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); @@ -119,17 +147,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=[]; @@ -200,6 +227,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); + } +}); /** @@ -265,7 +400,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; } @@ -302,7 +437,6 @@ overviewer.views.SignControlView = Backbone.View.extend({ }}); } - iconURL = overviewerConfig.CONST.image.signMarker; //dataRoot['markers'] = []; // for (i in dataRoot) { @@ -310,15 +444,20 @@ 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) { overviewer.util.createMarkerInfoWindow(marker); } jQuery.extend(entity, {markerObj: marker}); 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 0ae76e5..831a138 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..8ea0b52 100644 --- a/overviewer_core/rendermodes.py +++ b/overviewer_core/rendermodes.py @@ -188,6 +188,12 @@ class MineralOverlay(Overlay): '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 91c2e9b..caf9128 100644 --- a/overviewer_core/settingsDefinition.py +++ b/overviewer_core/settingsDefinition.py @@ -79,6 +79,7 @@ renders = Setting(required=True, default=util.OrderedDict(), "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=None), @@ -99,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..0df90d1 100644 --- a/overviewer_core/settingsValidators.py +++ b/overviewer_core/settingsValidators.py @@ -45,12 +45,24 @@ 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 "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)) diff --git a/overviewer_core/src/overviewer.h b/overviewer_core/src/overviewer.h index 1bb8f92..c9b7ae6 100644 --- a/overviewer_core/src/overviewer.h +++ b/overviewer_core/src/overviewer.h @@ -26,7 +26,7 @@ // 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 31 /* Python PIL, and numpy headers */ #include diff --git a/overviewer_core/src/primitives/base.c b/overviewer_core/src/primitives/base.c index 5a66a9a..ae93db7 100644 --- a/overviewer_core/src/primitives/base.c +++ b/overviewer_core/src/primitives/base.c @@ -206,6 +206,7 @@ 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; @@ -215,7 +216,7 @@ base_draw(void *data, RenderState *state, PyObject *src, PyObject *mask, PyObjec /* 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. @@ -257,6 +258,18 @@ base_draw(void *data, RenderState *state, PyObject *src, PyObject *mask, PyObjec r = PyInt_AsLong(PyTuple_GET_ITEM(color, 0)); g = PyInt_AsLong(PyTuple_GET_ITEM(color, 1)); b = PyInt_AsLong(PyTuple_GET_ITEM(color, 2)); + + /* swamp hack + All values are guessed. They are probably somewhat odd or + completely wrong, but this looks okay for me, and I'm male, + so I can only distinct about 10 different colors anyways. + Blame my Y-Chromosone. */ + if(biome == 6) { + r = PyInt_AsLong(PyTuple_GET_ITEM(color, 0)) * 0.8; + g = PyInt_AsLong(PyTuple_GET_ITEM(color, 1)) / 2.0; + b = PyInt_AsLong(PyTuple_GET_ITEM(color, 2)) * 1.0; + } + Py_DECREF(color); } diff --git a/overviewer_core/src/primitives/edge-lines.c b/overviewer_core/src/primitives/edge-lines.c index dc3d1c1..00f12ae 100644 --- a/overviewer_core/src/primitives/edge-lines.c +++ b/overviewer_core/src/primitives/edge-lines.c @@ -38,6 +38,7 @@ 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 @@ -46,15 +47,15 @@ edge_lines_draw(void *data, RenderState *state, PyObject *src, PyObject *mask, P 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/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/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/tileset.py b/overviewer_core/tileset.py index 4fa769f..1640290 100644 --- a/overviewer_core/tileset.py +++ b/overviewer_core/tileset.py @@ -32,6 +32,7 @@ from .util import roundrobin from . import nbt from .files import FileReplacer from .optimizeimages import optimize_image +import rendermodes import c_overviewer """ @@ -507,6 +508,8 @@ 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, @@ -519,7 +522,11 @@ class TileSet(object): (" - " + 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: