diff --git a/CONTRIBUTORS.rst b/CONTRIBUTORS.rst new file mode 100644 index 0000000..e251b41 --- /dev/null +++ b/CONTRIBUTORS.rst @@ -0,0 +1,53 @@ +============ +Contributors +============ + +This file contains a list of every person who has contributed code to +Overviewer. It was created from the git commit log, and should include +everyone, but we may have missed a few. + +Not currently included (but hopefully soon) are countless testers and bug +reporters that helped fixed the many bugs that have popped up in the course of +development. + +--------------- +Original Author +--------------- + + * Andrew Brown + +------------------------- +Long-term Contributions +------------------------- + +These contributors have made many changes, over a fairly long time span, or +for many different parts of the code. + + * Alejandro Aguilera + * Andrew Chin + * Aaron Griffith + * Alex Headley + * Alex Jurkiewicz + * Xon + +------------------------ +Short-term Contributions +------------------------ + +These contributors have made specific changes for a particular bug fix or +feature. + + * arrai + * Kyle Brantley + * cbarber + * Alex Cline + * Stephen Fluin + * Benjamin Herr + * Ryan Hitchman + * Jenny + * Michael Jensen + * Ryan McCue + * Morlok8k + * Gregory Short + * Sam Steele + * timwolla diff --git a/README.rst b/README.rst index 2b07d45..1d0e9ab 100644 --- a/README.rst +++ b/README.rst @@ -1,21 +1,26 @@ ==================== Minecraft Overviewer ==================== -By Andrew Brown and contributors +By Andrew Brown and contributors (see CONTRIBUTORS.rst). http://github.com/brownan/Minecraft-Overviewer Generates large resolution images of a Minecraft map. In short, this program reads in Minecraft world files and renders very large -resolution images. It performs a similar function to the existing Minecraft -Cartographer program but with a slightly different goal in mind: to generate -large resolution images such that one can zoom in and see details. +resolution images that can be viewed through a Google Maps interface. It +performs a similar function to the existing Minecraft Cartographer program but +with a slightly different goal in mind: to generate large resolution images +such that one can zoom in and see details. See some examples here! http://github.com/brownan/Minecraft-Overviewer/wiki/Map-examples -(To contact me, send me a message on Github) +Further documentation may be found at +https://github.com/brownan/Minecraft-Overviewer/wiki/Documentation + +To contact the developers and other users, go to the site at the top of this +README, or go to #overviewer on irc.freenode.net. Features ======== @@ -56,6 +61,12 @@ If something doesn't work, let me know. Using the Overviewer ==================== +For a quick-start guide, see +https://github.com/brownan/Minecraft-Overviewer/wiki/Quick-Start-Guide + +If you are upgrading from an older Overviewer to the new DTT code, see +https://github.com/brownan/Minecraft-Overviewer/wiki/DTT-Upgrade-Guide + Disclaimers ----------- Before you dive into using this, just be aware that, for large maps, there is a @@ -89,7 +100,7 @@ you can use the Overviewer: texture packs out there. Biome Tinting -~~~~~~~~~~~~~ +------------- With the Halloween update, biomes were added to Minecraft. In order to get biome-accurate tinting, the Overviewer can use biome data produced by the Minecraft Biome Extractor tool. This tool can be downloaded from: @@ -102,12 +113,12 @@ then the Overviewer will use a static tinting for grass and leaves. Compiling the C Extension ------------------------- -The C Extension for Overviewer is no longer optional. In addition to providing -a higher quality image compositing function that looks better on maps with lighting -enabled, it now does the bulk of the rendering. +The C Extension for Overviewer is no longer optional. In addition to +providing a higher quality image compositing function that looks better on +maps with lighting enabled, it now does the bulk of the rendering. -If you downloaded Overviewer as a binary package, this extension will already be -compiled for you. +If you downloaded Overviewer as a binary package, this extension will already +be compiled for you. If you have a C compiler and the Python development libraries set up, you can compile this extension like this:: @@ -119,7 +130,7 @@ look for a package named 'python-dev', 'python-devel' or similar. Also, some Python distributions do not install "Imaging.h" and "ImPlatform.h" properly. If you get errors complaining about them, you can get them from the PIL source, or at . Just put them in -the same directory as "_composite.c". +the same directory as "overviewer.py". For more detailed instructions, check the wiki: https://github.com/brownan/Minecraft-Overviewer/wiki/Build-Instructions @@ -142,12 +153,6 @@ Options -h, --help Shows the list of options and exits ---imgformat=FORMAT - Set the output image format used for the tiles. The default is 'png', - but 'jpg' is also supported. Note that regardless of what you choose, - Overviewer will still use PNG for cached images to avoid recompression - artifacts. - -p PROCS, --processes=PROCS Adding the "-p" option will utilize more cores during processing. This can speed up rendering quite a bit. The default is set to the same @@ -157,34 +162,6 @@ Options python overviewer.py -p 5 --z ZOOM, --zoom=ZOOM - The Overviewer by default will detect how many zoom levels are required - to show your entire map. This option sets it manually. - - *You do not normally need to set this option!* - - This is equivalent to setting the dimensions of the highest zoom level. It - does not actually change how the map is rendered, but rather *how much of - the map is rendered.* (Calling this option "zoom" may be a bit misleading, - I know) - - To be precise, it sets the width and height of the highest zoom level, in - tiles. A zoom level of z means the highest zoom level of your map will be - 2^z by 2^z tiles. - - This option map be useful if you have some outlier chunks causing your map - to be too large, or you want to render a smaller portion of your map, - instead of rendering everything. - - This will render your map with 7 zoom levels:: - - python overviewer.py -z 7 - - Remember that each additional zoom level adds 4 times as many tiles as - the last. This can add up fast, zoom level 10 has over a million tiles. - Tiles with no content will not be rendered, but they still take a small - amount of time to process. - -d, --delete This option changes the mode of execution. No tiles are rendered, and instead, files are deleted. @@ -204,38 +181,65 @@ Options a certain date. Or perhaps you can incrementally update your map by passing in a subset of regions each time. It's up to you! ---lighting - This option enables map lighting, using lighting information stored by - Minecraft inside the chunks. This will make your map prettier, at the cost - of update speed. - - Note that for existing, unlit maps, you may want to clear your cache - (with -d) before updating the map to use lighting. Otherwise, only updated - chunks will have lighting enabled. +--rendermodes=MODE1[,MODE2,...] + Use this option to specify which render mode to use, such as lighting or + night. Use --list-rendermodes to get a list of available rendermodes, and + a short description of each. If you provide more than one mode (separated + by commas), Overviewer will render all of them at once, and provide a + toggle on the resulting map to switch between them. ---night - This option enables --lighting, and renders the world at night. - ---web-assets-hook=HOOK - This option lets you specify a script to run after the web assets have been - copied into the output directory, but before any tile rendering takes - place. This is an ideal time to do any custom postprocessing for markers.js - or other web assets. - - The script should be executable, and it should accept one argument: - the path to the output directory. +--list-rendermodes + List the available render modes, and a short description of each. Settings -------- - You can optionally store settings in a file named settings.py. It is a regular python script, so you can use any python functions or modules you want. -This section needs to be expanded +For a sample settings file, look at 'sample.settings.py'. Note that this file +is not meant to be used directly, but instead it should be used as a +collection of examples to guide writing your own. -For a sample settings file, look at sample.settings.py +Here's a (possibly incomplete) list of available settings, which are available +in settings.py. Note that you can also set command-line options in a similar +way. +imgformat=FORMAT + Set the output image format used for the tiles. The default is 'png', + but 'jpg' is also supported. + +zoom=ZOOM + The Overviewer by default will detect how many zoom levels are required + to show your entire map. This option sets it manually. + + *You do not normally need to set this option!* + + This is equivalent to setting the dimensions of the highest zoom level. It + does not actually change how the map is rendered, but rather *how much of + the map is rendered.* (Calling this option "zoom" may be a bit misleading, + I know) + + To be precise, it sets the width and height of the highest zoom level, in + tiles. A zoom level of z means the highest zoom level of your map will be + 2^z by 2^z tiles. + + This option map be useful if you have some outlier chunks causing your map + to be too large, or you want to render a smaller portion of your map, + instead of rendering everything. + + Remember that each additional zoom level adds 4 times as many tiles as + the last. This can add up fast, zoom level 10 has over a million tiles. + Tiles with no content will not be rendered, but they still take a small + amount of time to process. + +web_assets_hook + This option lets you define a function to run after the web assets have + been copied into the output directory, but before any tile rendering takes + place. This is an ideal time to do any custom postprocessing for + markers.js or other web assets. + + This function should accept one argument: a QuadtreeGen object. Viewing the Results ------------------- diff --git a/overviewer.py b/overviewer.py index a80ef27..698de48 100755 --- a/overviewer.py +++ b/overviewer.py @@ -96,7 +96,9 @@ def main(): parser.add_option("--chunklist", dest="chunklist", help="A file containing, on each line, a path to a chunkfile to update. Instead of scanning the world directory for chunks, it will just use this list. Normal caching rules still apply.") parser.add_option("--rendermodes", dest="rendermode", help="Specifies the render types, separated by commas. Use --list-rendermodes to list them all.", type="choice", choices=avail_rendermodes, required=True, default=avail_rendermodes[0], listify=True) parser.add_option("--list-rendermodes", dest="list_rendermodes", action="store_true", help="List available render modes and exit.", commandLineOnly=True) - parser.add_option("--imgformat", dest="imgformat", help="The image output format to use. Currently supported: png(default), jpg. NOTE: png will always be used as the intermediate image format.", configFileOnly=True ) + parser.add_option("--imgformat", dest="imgformat", help="The image output format to use. Currently supported: png(default), jpg.", configFileOnly=True ) + parser.add_option("--imgquality", dest="imgquality", default=95, help="Specify the quality of image output when using imgformat=\"jpg\".", type="int", configFileOnly=True) + parser.add_option("--bg_color", dest="bg_color", help="Configures the background color for the GoogleMap output. Specify in #RRGGBB format", configFileOnly=True, type="string", default="#1A1A1A") parser.add_option("--optimize-img", dest="optimizeimg", help="If using png, perform image file size optimizations on the output. Specify 1 for pngcrush, 2 for pngcrush+optipng+advdef. This may double (or more) render times, but will produce up to 30% smaller images. NOTE: requires corresponding programs in $PATH or %PATH%", configFileOnly=True) parser.add_option("--web-assets-hook", dest="web_assets_hook", help="If provided, run this function after the web assets have been copied, but before actual tile rendering begins. It should accept a QuadtreeGen object as its only argument.", action="store", metavar="SCRIPT", type="function", configFileOnly=True) parser.add_option("-q", "--quiet", dest="quiet", action="count", default=0, help="Print less output. You can specify this option multiple times.") @@ -162,10 +164,18 @@ def main(): logging.error("Invalid world number") sys.exit(1) - if len(args) != 2: + if len(args) < 2: if options.delete: return delete_all(worlddir, None) - parser.error("Where do you want to save the tiles?") + logging.error("Where do you want to save the tiles?") + sys.exit(1) + elif len(args) > 2: + if options.delete: + return delete_all(worlddir, None) + parser.print_help() + logging.error("Sorry, you specified too many arguments") + sys.exit(1) + destdir = args[1] if options.display_config: @@ -214,27 +224,34 @@ def main(): logging.info("Rending the following tilesets: %s", ",".join(options.rendermode)) + bgcolor = (int(options.bg_color[1:3],16), int(options.bg_color[3:5],16), int(options.bg_color[5:7],16), 0) + # create the quadtrees # TODO chunklist q = [] - qtree_args = {'depth' : options.zoom, 'imgformat' : imgformat, 'optimizeimg' : optimizeimg} + qtree_args = {'depth' : options.zoom, 'imgformat' : imgformat, 'imgquality' : options.imgquality, 'optimizeimg' : optimizeimg, 'bgcolor' : bgcolor} for rendermode in options.rendermode: if rendermode == 'normal': qtree = quadtree.QuadtreeGen(w, destdir, rendermode=rendermode, tiledir='tiles', **qtree_args) else: qtree = quadtree.QuadtreeGen(w, destdir, rendermode=rendermode, **qtree_args) q.append(qtree) + + # do quadtree-level preprocessing + for qtree in q: + qtree.go(options.procs) - #create the distributed render + # create the distributed render r = rendernode.RenderNode(q) # write out the map and web assets - m = googlemap.MapGen(q, skipjs=options.skipjs, web_assets_hook=options.web_assets_hook) + m = googlemap.MapGen(q, configInfo=options) m.go(options.procs) # render the tiles! r.go(options.procs) + # finish up the map m.finalize() diff --git a/overviewer_core/chunk.py b/overviewer_core/chunk.py index 977fdfa..048b650 100644 --- a/overviewer_core/chunk.py +++ b/overviewer_core/chunk.py @@ -114,13 +114,15 @@ def get_tileentity_data(level): return data # This set holds blocks ids that can be seen through, for occlusion calculations -transparent_blocks = set([0, 6, 8, 9, 18, 20, 37, 38, 39, 40, 44, 50, 51, 52, 53, 55, - 59, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 74, 75, 76, 77, 78, 79, 81, 83, 85, 92]) +transparent_blocks = set([ 0, 6, 8, 9, 18, 20, 26, 27, 28, 30, 37, 38, 39, 40, + 44, 50, 51, 52, 53, 55, 59, 63, 64, 65, 66, 67, 68, 69, + 70, 71, 72, 74, 75, 76, 77, 78, 79, 81, 83, 85, 90, 92, + 93, 94]) # This set holds block ids that are solid blocks solid_blocks = set([1, 2, 3, 4, 5, 7, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 35, 41, 42, 43, 44, 45, 46, 47, 48, 49, 53, 54, 56, 57, 58, 60, - 61, 62, 64, 65, 66, 67, 71, 73, 74, 78, 79, 80, 81, 82, 84, 86, 87, 88, 89, 91, 92]) + 61, 62, 67, 73, 74, 78, 79, 80, 81, 82, 84, 86, 87, 88, 89, 91]) # This set holds block ids that are fluid blocks fluid_blocks = set([8,9,10,11]) @@ -284,7 +286,14 @@ class ChunkRenderer(object): self._load_up_right() return self._up_right_blocks up_right_blocks = property(_load_up_right_blocks) - + + def _load_up_right_skylight(self): + """Loads and returns lower-right skylight array""" + if not hasattr(self, "_up_right_skylight"): + self._load_up_right() + return self._up_right_skylight + up_right_skylight = property(_load_up_right_skylight) + def _load_up_left(self): """Loads and sets data from upper-left chunk""" chunk_path = self.world.get_region_path(self.chunkX, self.chunkY - 1) @@ -305,6 +314,13 @@ class ChunkRenderer(object): return self._up_left_blocks up_left_blocks = property(_load_up_left_blocks) + def _load_up_left_skylight(self): + """Loads and returns lower-right skylight array""" + if not hasattr(self, "_up_left_skylight"): + self._load_up_left() + return self._up_left_skylight + up_left_skylight = property(_load_up_left_skylight) + def generate_pseudo_ancildata(self,x,y,z,blockid, north_position = 0 ): """ Generates a pseudo ancillary data for blocks that depend of what are surrounded and don't have ancillary data @@ -449,7 +465,7 @@ def generate_facemasks(): return (top, left, right) facemasks = generate_facemasks() black_color = Image.new("RGB", (24,24), (0,0,0)) -red_color = Image.new("RGB", (24,24), (229,36,38)) +white_color = Image.new("RGB", (24,24), (255,255,255)) # Render 128 different color images for color coded depth blending in cave mode def generate_depthcolors(): @@ -458,8 +474,10 @@ def generate_depthcolors(): g = 0 b = 0 for z in range(128): - img = Image.new("RGB", (24,24), (r,g,b)) - depth_colors.append(img) + depth_colors.append(r) + depth_colors.append(g) + depth_colors.append(b) + if z < 32: g += 7 elif z < 64: diff --git a/overviewer_core/configParser.py b/overviewer_core/configParser.py index 88925d7..5b275f0 100644 --- a/overviewer_core/configParser.py +++ b/overviewer_core/configParser.py @@ -4,7 +4,8 @@ import os.path import logging class OptionsResults(object): - pass + def get(self, *args): + return self.__dict__.get(*args) class ConfigOptionParser(object): def __init__(self, **kwargs): diff --git a/overviewer_core/data/config.js b/overviewer_core/data/config.js deleted file mode 100644 index 78dfd7d..0000000 --- a/overviewer_core/data/config.js +++ /dev/null @@ -1,55 +0,0 @@ - - var config = { - fileExt: '{imgformat}', - tileSize: 384, - defaultZoom: 2, - maxZoom: {maxzoom}, - // center on this point, in world coordinates, ex: - //center: [0,0,0], - center: {spawn_coords}, - cacheMinutes: 0, // Change this to have browsers automatically request new images every x minutes - bg_color: '#1A1A1A', - debug: false - }; - - -/* signGroups -- A list of signpost groups. A signpost can fall into zero, one, or more than one - * group. See below for some examples. - * - * Required: - * label : string. Displayed in the drop down menu control. - * match : function. Applied to each marker (from markers.js). It is returns true if the marker - * Should be part of the group. - * - * Optional: - * checked : boolean. Set to true to have the group visible by default - * icon : string. Used to specify an icon url. - */ -var signGroups = [ -// {label: "'To'", checked: false, match: function(s) {return s.msg.match(/to/)}}, -// {label: "Storage", match: function(s) {return s.msg.match(/storage/i) || s.msg.match(/dirt/i) || s.msg.match(/sand/)}}, -// {label: "Below Sealevel", match: function(s) { return s.y<64;}}, -// {label: "Info", match: function(s) { return s.msg.match("\\[info\\]");}, icon:"http://google-maps-icons.googlecode.com/files/info.png"}, - {label: "All", match: function(s) {return true}}, -]; - -/* mapTypeData -- a list of alternate map renderings available. At least one rendering must be - * listed. When more than one are provided, controls to switch between them are provided, with - * the first one being the default. - * - * Required: - * label : string. Displayed on the control. - * path : string. Location of the rendered tiles. - * Optional: - * base : string. Base of the url path for tile locations, useful for serving tiles from a different server than the js/html server. - -var mapTypeData=[ - {'label': 'Unlit', 'path': 'tiles'}, -// {'label': 'Day', 'path': 'lighting/tiles'}, -// {'label': 'Night', 'path': 'night/tiles'}, -// {'label': 'Spawn', 'path': 'spawn/tiles', 'base': 'http://example.cdn.amazon.com/'} -]; - */ - -var mapTypeData = {maptypedata}; - diff --git a/overviewer_core/data/overviewerConfig.js b/overviewer_core/data/overviewerConfig.js new file mode 100644 index 0000000..acb4529 --- /dev/null +++ b/overviewer_core/data/overviewerConfig.js @@ -0,0 +1,157 @@ +var overviewerConfig = { + /** + * These are things that will probably not need to be changed by the user, + * but are there because otherwise changing them is a giant PITA. + */ + 'CONST': { + /** + * Height and width of the tiles in pixels (I think). + */ + 'tileSize': 384, + /** + * Various images used for markers and stuff. + */ + 'image': { + 'defaultMarker': 'signpost.png', + 'signMarker': 'signpost_icon.png', + 'compass': 'compass.png', + 'spawnMarker': 'http://google-maps-icons.googlecode.com/files/home.png', + 'queryMarker': 'http://google-maps-icons.googlecode.com/files/regroup.png' + }, + 'mapDivId': 'mcmap', + 'regionStrokeWeight': 2 + }, + /** + * General map settings. + */ + 'map': { + /** + * Control the visibility of various controls. + */ + 'controls': { + /** + * Pan control is the hand with the arrows around it in the upper left. + */ + 'pan': true, + /** + * Zoom control is the zoom slider bar in the upper left. + */ + 'zoom': true, + /** + * Spawn control is the "Spawn" button that centers the map on spawn. + */ + 'spawn': true, + /** + * The compass in the upper right. + */ + 'compass': true, + /** + * The mapType control is the slider for selecting different map types. + */ + 'mapType': true, + /** + * The small box at the bottom that displays the link to the current map view. + */ + 'link': true + }, + /** + * The zoom level when the page is loaded without a specific zoom setting + */ + 'defaultZoom': 0, + /** + * This controls how far you can zoom out. + */ + 'minZoom': {minzoom}, + /** + * This controls how close you can zoom in. + */ + 'maxZoom': {maxzoom}, + /** + * Center on this point, in world coordinates. Should be an array, ex: + * [0,0,0] + */ + 'center': {spawn_coords}, + /** + * Set this to tell browsers how long they should cache tiles in minutes. + */ + 'cacheMinutes': 0, + /** + * Set to true to turn on debug mode, which adds a grid to the map along + * with co-ordinates and a bunch of console output. + */ + 'debug': false, + }, + /** + * Group definitions for objects that are partially selectable (signs and + * regions). + */ + 'objectGroups': { + /* signs -- A list of signpost groups. A signpost can fall into zero, + * one, or more than one group. See below for some examples. + * + * Required: + * label : string. Displayed in the drop down menu control. + * match : function. Applied to each marker (from markers.js). It + * is returns true if the marker should be part + * of the group. + * + * Optional: + * checked : boolean. Set to true to have the group visible by default + * icon : string. Used to specify an icon url. + */ + 'signs': [ + //{label: "'To'", checked: false, match: function(s) {return s.msg.match(/to/)}}, + //{label: "Storage", match: function(s) {return s.msg.match(/storage/i) || s.msg.match(/dirt/i) || s.msg.match(/sand/)}}, + //{label: "Below Sealevel", match: function(s) { return s.y<64;}}, + //{label: "Info", match: function(s) { return s.msg.match("\\[info\\]");}, icon:"http://google-maps-icons.googlecode.com/files/info.png"}, + {'label':'All', 'match':function(sign){return true;}} + ], + /* regions -- A list of region groups. A region can fall into zero, + * one, or more than one group. See below for some examples. + * Regions have been designed to work with the WorldGuard Overviewer + * Region importer at @link http://goo.gl/dc0tV but your + * host must support php in order to run WG2OvR. You can also continue + * to use any other region format. + * + * Required: + * label : string. Displayed in the drop down menu control. + * clickable : boolean. Will determine if we should generate an + * experimental info window that shows details + * about the clicked region. + * NOTE: if a region (as defined in region.js) + * does not have a label, this will default to + * false. + * match : function. Applied to each region (from region.js). It + * returns true if the region should be part of + * the group. + * + * Optional: + * checked : boolean. Set to true to have the group visible by default + */ + 'regions': [ + //{'label':'All','clickable':true,'match':function(region){return true;}} + ] + }, + /* mapTypes -- a list of alternate map renderings available. At least one + * rendering must be listed. When more than one are provided, controls to + * switch between them are provided, with the first one being the default. + * + * Required: + * label : string. Displayed on the control. + * path : string. Location of the rendered tiles. + * Optional: + * base : string. Base of the url path for tile locations, useful + * for serving tiles from a different server than + * the js/html server. + * imgformat : string. File extension used for these tiles. Defaults to png. + * overlay : bool. If true, this tile set will be treated like an overlay + * Example: + * 'mapTypes': [ + * {'label': 'Day', 'path': 'lighting/tiles'}, + * {'label': 'Night', 'path': 'night/tiles', 'imgformat': 'jpg'}, + * {'label': 'Spawn', 'path': 'spawn/tiles', 'base': 'http://example.cdn.amazon.com/'}, + * {'label': 'Overlay', 'path': 'overlay/tiles', 'overlay': true} + * ] + */ + 'mapTypes': {maptypedata} +}; diff --git a/overviewer_core/data/textures/portal.png b/overviewer_core/data/textures/portal.png new file mode 100644 index 0000000..1f75ca3 Binary files /dev/null and b/overviewer_core/data/textures/portal.png differ diff --git a/overviewer_core/data/web_assets/functions.js b/overviewer_core/data/web_assets/functions.js deleted file mode 100644 index 65a78d4..0000000 --- a/overviewer_core/data/web_assets/functions.js +++ /dev/null @@ -1,468 +0,0 @@ -var markerCollection = {}; // holds groups of markers - -var map; - -var markersInit = false; -var regionsInit = false; - -var prevInfoWindow = null; - -function prepareSignMarker(marker, item) { - - var c = "

" + item.msg.replace(/\n/g,"
") + "

"; - var infowindow = new google.maps.InfoWindow({content: c - }); - google.maps.event.addListener(marker, 'click', function() { - if (prevInfoWindow) - prevInfoWindow.close() - infowindow.open(map,marker); - prevInfoWindow = infowindow - }); - -} - - -function drawMapControls() { - - // viewstate link - var viewStateDiv = document.createElement('DIV'); - - // - - viewStateDiv.id="link"; - - - map.controls[google.maps.ControlPosition.BOTTOM_RIGHT].push(viewStateDiv); - - - // compass rose, in the top right corner - var compassDiv = document.createElement('DIV'); - - compassDiv.style.padding = '5px'; - - var compassImg = document.createElement('IMG'); - compassImg.src="compass.png"; - compassDiv.appendChild(compassImg); - - compassDiv.index = 0; - map.controls[google.maps.ControlPosition.TOP_RIGHT].push(compassDiv); - - - if (signGroups.length > 0) { - // signpost display control - // - - var signControl = document.createElement("DIV"); - signControl.id = "signControl"; // let's let a style sheet do most of the styling here - - var controlBorder = document.createElement("DIV"); - controlBorder.id="top"; - signControl.appendChild(controlBorder); - - var controlText = document.createElement("DIV"); - - controlBorder.appendChild(controlText); - - controlText.innerHTML = "Signposts"; - - var dropdownDiv = document.createElement("DIV"); - - - $(controlText).click(function() { - $(dropdownDiv).toggle(); - - }); - - - dropdownDiv.id="dropDown"; - signControl.appendChild(dropdownDiv); - dropdownDiv.innerHTML=""; - - map.controls[google.maps.ControlPosition.TOP_RIGHT].push(signControl); - - - - var hasSignGroup = false; - for (idx in signGroups) { - var item = signGroups[idx]; - //console.log(item); - label = item.label; - hasSignGroup = true; - var d = document.createElement("div"); - var n = document.createElement("input"); - n.type="checkbox"; - - $(n).data("label",label); - jQuery(n).click(function(e) { - var t = $(e.target); - jQuery.each(markerCollection[t.data("label")], function(i,elem) {elem.setVisible(e.target.checked);}); - }); - - - if (item.checked) { - n.checked = true; - jQuery.each(markerCollection[label], function(i,elem) {elem.setVisible(n.checked);}); - } - dropdownDiv.appendChild(d); - d.appendChild(n) - var textNode = document.createElement("text"); - textNode.innerHTML = label + "
"; - d.appendChild(textNode); - - } - - - } -} - -function initRegions() { - if (regionsInit) { return; } - - regionsInit = true; - - for (i in regionData) { - var region = regionData[i]; - var converted = new google.maps.MVCArray(); - for (j in region.path) { - var point = region.path[j]; - converted.push(fromWorldToLatLng(point.x, point.y, point.z)); - } - - if (region.closed) { - new google.maps.Polygon({clickable: false, - geodesic: false, - map: map, - strokeColor: region.color, - strokeOpacity: region.opacity, - strokeWeight: 2, - fillColor: region.color, - fillOpacity: region.opacity * 0.25, - zIndex: i, - paths: converted - }); - } else { - new google.maps.Polyline({clickable: false, - geodesic: false, - map: map, - strokeColor: region.color, - strokeOpacity: region.opacity, - strokeWeight: 2, - zIndex: i, - path: converted - }); - } - } -} - - - -function initMarkers() { - if (markersInit) { return; } - - markersInit = true; - - // first, give all collections an empty array to work with - for (i in signGroups) { - markerCollection[signGroups[i].label] = []; - } - - for (i in markerData) { - var item = markerData[i]; - - // a default: - var iconURL = ''; - if (item.type == 'spawn') { - // don't filter spawn, always display - - iconURL = 'http://google-maps-icons.googlecode.com/files/home.png'; - var converted = fromWorldToLatLng(item.x, item.y, item.z); - var marker = new google.maps.Marker({position: converted, - map: map, - title: jQuery.trim(item.msg), - icon: iconURL - }); - - continue; - } - - var matched = false; - for (idx in signGroups) { - var signGroup = signGroups[idx]; - var testfunc = signGroup.match; - var label = signGroup.label; - - if (testfunc(item)) { - matched = true; - - if (item.type == 'sign') { iconURL = 'signpost_icon.png';} - - //console.log(signGroup.icon); - if (signGroup.icon) { iconURL = signGroup.icon; - } - - var converted = fromWorldToLatLng(item.x, item.y, item.z); - var marker = new google.maps.Marker({position: converted, - map: map, - title: jQuery.trim(item.msg), - icon: iconURL, - visible: false - }); - - markerCollection[label].push(marker); - - if (item.type == 'sign') { - prepareSignMarker(marker, item); - } - - } - - - } - - if (!matched) { - // is this signpost doesn't match any of the groups in config.js, add it automatically to the "__others__" group - if (item.type == 'sign') { iconURL = 'signpost_icon.png';} - - var converted = fromWorldToLatLng(item.x, item.y, item.z); - var marker = new google.maps.Marker({position: converted, - map: map, - title: jQuery.trim(item.msg), - icon: iconURL, - visible: false - }); - if (markerCollection["__others__"]) { - markerCollection["__others__"].push(marker); - } else { - markerCollection["__others__"] = [marker]; - } - - if (item.type == 'sign') { - prepareSignMarker(marker, item); - } - } - - - } -} - - -function makeLink() { - var a=location.href.substring(0,location.href.lastIndexOf(location.search)) - + "?lat=" + map.getCenter().lat().toFixed(6) - + "&lng=" + map.getCenter().lng().toFixed(6) - + "&zoom=" + map.getZoom(); - document.getElementById("link").innerHTML = a; -} - -function initialize() { - - var query = location.search.substring(1); - - var defaultCenter = fromWorldToLatLng(config.center[0], - config.center[1], - config.center[2]); - var lat = defaultCenter.lat(); - var lng = defaultCenter.lng(); - - var zoom = config.defaultZoom; - var pairs = query.split("&"); - for (var i=0; i 1) { - mapTyepControlToggle = true - } - var mapOptions = { - zoom: zoom, - center: new google.maps.LatLng(lat, lng), - navigationControl: true, - scaleControl: false, - mapTypeControl: mapTyepControlToggle, - mapTypeControlOptions: { - mapTypeIds: mapTypeIds - }, - mapTypeId: mapTypeIdDefault, - streetViewControl: false, - backgroundColor: config.bg_color, - }; - map = new google.maps.Map(document.getElementById('mcmap'), mapOptions); - - if(config.debug) { - map.overlayMapTypes.insertAt(0, new CoordMapType(new google.maps.Size(config.tileSize, config.tileSize))); - - google.maps.event.addListener(map, 'click', function(event) { - //console.log("latLng; " + event.latLng.lat() + ", " + event.latLng.lng()); - - var pnt = map.getProjection().fromLatLngToPoint(event.latLng); - //console.log("point: " + pnt); - - var pxx = pnt.x * config.tileSize * Math.pow(2, config.maxZoom); - var pxy = pnt.y * config.tileSize * Math.pow(2, config.maxZoom); - //console.log("pixel: " + pxx + ", " + pxy); - }); - } - - // Now attach the coordinate map type to the map's registry - for (idx in MCMapType) { - map.mapTypes.set('mcmap' + MCMapType[idx].name, MCMapType[idx]); - } - - // We can now set the map to use the 'coordinate' map type - map.setMapTypeId(mapTypeIdDefault); - - // initialize the markers and regions - initMarkers(); - initRegions(); - drawMapControls(); - - //makeLink(); - - // Make the link again whenever the map changes - google.maps.event.addListener(map, 'zoom_changed', function() { - makeLink(); - }); - google.maps.event.addListener(map, 'center_changed', function() { - makeLink(); - }); - -} - - - // our custom projection maps Latitude to Y, and Longitude to X as normal, - // but it maps the range [0.0, 1.0] to [0, tileSize] in both directions - // so it is easier to position markers, etc. based on their position - // (find their position in the lowest-zoom image, and divide by tileSize) - function MCMapProjection() { - this.inverseTileSize = 1.0 / config.tileSize; - } - - MCMapProjection.prototype.fromLatLngToPoint = function(latLng) { - var x = latLng.lng() * config.tileSize; - var y = latLng.lat() * config.tileSize; - return new google.maps.Point(x, y); - }; - - MCMapProjection.prototype.fromPointToLatLng = function(point) { - var lng = point.x * this.inverseTileSize; - var lat = point.y * this.inverseTileSize; - return new google.maps.LatLng(lat, lng); - }; - - // helper to get map LatLng from world coordinates - // takes arguments in X, Y, Z order - // (arguments are *out of order*, because within the function we use - // the axes like the rest of Minecraft Overviewer -- with the Z and Y - // flipped from normal minecraft usage.) - function fromWorldToLatLng(x, z, y) - { - // the width and height of all the highest-zoom tiles combined, inverted - var perPixel = 1.0 / (config.tileSize * Math.pow(2, config.maxZoom)); - - // This information about where the center column is may change with a different - // drawing implementation -- check it again after any drawing overhauls! - - // point (0, 0, 127) is at (0.5, 0.0) of tile (tiles/2 - 1, tiles/2) - // so the Y coordinate is at 0.5, and the X is at 0.5 - ((tileSize / 2) / (tileSize * 2^maxZoom)) - // or equivalently, 0.5 - (1 / 2^(maxZoom + 1)) - var lng = 0.5 - (1.0 / Math.pow(2, config.maxZoom + 1)); - var lat = 0.5; - - // the following metrics mimic those in ChunkRenderer.chunk_render in "chunk.py" - // or, equivalently, chunk_render in src/iterate.c - - // each block on X axis adds 12px to x and subtracts 6px from y - lng += 12 * x * perPixel; - lat -= 6 * x * perPixel; - - // each block on Y axis adds 12px to x and adds 6px to y - lng += 12 * y * perPixel; - lat += 6 * y * perPixel; - - // each block down along Z adds 12px to y - lat += 12 * (128 - z) * perPixel; - - // add on 12 px to the X coordinate to center our point - lng += 12 * perPixel; - - return new google.maps.LatLng(lat, lng); - } - -function getTileUrlGenerator(path, path_base) { - return function(tile, zoom) { - var url = path; - var url_base = ( path_base ? path_base : '' ); - if(tile.x < 0 || tile.x >= Math.pow(2, zoom) || tile.y < 0 || tile.y >= Math.pow(2, zoom)) { - url += '/blank'; - } else if(zoom == 0) { - url += '/base'; - } else { - for(var z = zoom - 1; z >= 0; --z) { - var x = Math.floor(tile.x / Math.pow(2, z)) % 2; - var y = Math.floor(tile.y / Math.pow(2, z)) % 2; - url += '/' + (x + 2 * y); - } - } - url = url + '.' + config.fileExt; - if(config.cacheMinutes > 0) { - var d = new Date(); - url += '?c=' + Math.floor(d.getTime() / (1000 * 60 * config.cacheMinutes)); - } - return(url_base + url); - } -} - -var MCMapOptions = new Array; -var MCMapType = new Array; -var mapTypeIdDefault = null; -var mapTypeIds = []; -for (idx in mapTypeData) { - var view = mapTypeData[idx]; - - MCMapOptions[view.label] = { - getTileUrl: getTileUrlGenerator(view.path, view.base), - tileSize: new google.maps.Size(config.tileSize, config.tileSize), - maxZoom: config.maxZoom, - minZoom: 0, - isPng: !(config.fileExt.match(/^png$/i) == null) - }; - - MCMapType[view.label] = new google.maps.ImageMapType(MCMapOptions[view.label]); - MCMapType[view.label].name = view.label; - MCMapType[view.label].alt = "Minecraft " + view.label + " Map"; - MCMapType[view.label].projection = new MCMapProjection(); - if (mapTypeIdDefault == null) { - mapTypeIdDefault = 'mcmap' + view.label; - } - mapTypeIds.push('mcmap' + view.label); -} - - function CoordMapType() { - } - - function CoordMapType(tileSize) { - this.tileSize = tileSize; - } - - CoordMapType.prototype.getTile = function(coord, zoom, ownerDocument) { - var div = ownerDocument.createElement('DIV'); - div.innerHTML = "(" + coord.x + ", " + coord.y + ", " + zoom + ")"; - div.innerHTML += "
"; - div.innerHTML += MCMapOptions.getTileUrl(coord, zoom); - div.style.width = this.tileSize.width + 'px'; - div.style.height = this.tileSize.height + 'px'; - div.style.fontSize = '10'; - div.style.borderStyle = 'solid'; - div.style.borderWidth = '1px'; - div.style.borderColor = '#AAAAAA'; - return div; - }; diff --git a/overviewer_core/data/web_assets/index.html b/overviewer_core/data/web_assets/index.html index 89554e8..3798f17 100644 --- a/overviewer_core/data/web_assets/index.html +++ b/overviewer_core/data/web_assets/index.html @@ -2,18 +2,16 @@ - - - + - + + + - -
+ +
diff --git a/overviewer_core/data/web_assets/style.css b/overviewer_core/data/web_assets/overviewer.css similarity index 69% rename from overviewer_core/data/web_assets/style.css rename to overviewer_core/data/web_assets/overviewer.css index e6029dc..19b9b34 100644 --- a/overviewer_core/data/web_assets/style.css +++ b/overviewer_core/data/web_assets/overviewer.css @@ -1,6 +1,18 @@ -html { height: 100% } -body { height: 100%; margin: 0px; padding: 0px ; background-color: #000; } -#mcmap { height: 100% } +html { + height: 100%; +} + +body { + height: 100%; + margin: 0px; + padding: 0px; + background-color: #000; +} + +#mcmap { + width: 100%; + height: 100%; +} .infoWindow { height: 100px; @@ -17,13 +29,13 @@ body { height: 100%; margin: 0px; padding: 0px ; background-color: #000; } font-family: monospace; } -#signControl { +#customControl { padding: 5px; height: 15px; font-family: Arial, sans-serif; } -#signControl > div#top { +#customControl > div#top { background-color: #fff; border: 2px solid #000; text-align: center; @@ -33,7 +45,14 @@ body { height: 100%; margin: 0px; padding: 0px ; background-color: #000; } cursor: pointer; } -#signControl > div#dropDown { +#customControl > div#dropDown { + border: 1px solid #000; + font-size: 12px; + background-color: #fff; + display: none; +} + +#customControl > div#button { border: 1px solid #000; font-size: 12px; background-color: #fff; diff --git a/overviewer_core/data/web_assets/overviewer.js b/overviewer_core/data/web_assets/overviewer.js new file mode 100644 index 0000000..eea2f7d --- /dev/null +++ b/overviewer_core/data/web_assets/overviewer.js @@ -0,0 +1,903 @@ +var overviewer = { + /** + * This holds the map, probably the most important var in this file + */ + 'map': null, + /** + * These are collections of data used in various places + */ + 'collections': { + /** + * A list of lists of raw marker data objects, this will allow for an + * arbitrary number of marker data sources. This replaces the old + * markerData var from markers.js. Now you can add markers by including + * a file with: + * overviewer.collections.markerDatas.push([]); + */ + 'markerDatas': [], + /** + * The actual Marker objects are stored here. + */ + 'markers': {}, + /** + * Same as markerDatas, list of lists of raw region objects. + */ + 'regionDatas': [], + /** + * The actual Region objects. + */ + 'regions': {}, + /** + * Overlay mapTypes (like Spawn) will go in here. + */ + 'overlays': [], + /** + * MapTypes that aren't overlays will end up in here. + */ + 'mapTypes': {}, + /** + * The mapType names are in here. + */ + 'mapTypeIds': [], + /** + * This is the current infoWindow object, we keep track of it so that + * there is only one open at a time. + */ + 'infoWindow': null + }, + 'util': { + /** + * General initialization function, called when the page is loaded. + * Probably shouldn't need changing unless some very different kind of new + * feature gets added. + */ + 'initialize': function() { + overviewer.util.initializeClassPrototypes(); + overviewer.util.initializeMapTypes(); + overviewer.util.initializeMap(); + overviewer.util.initializeMarkers(); + overviewer.util.initializeRegions(); + overviewer.util.createMapControls(); + }, + /** + * This adds some methods to these classes because Javascript is stupid + * and this seems like the best way to avoid re-creating the same methods + * on each object at object creation time. + */ + 'initializeClassPrototypes': function() { + overviewer.classes.MapProjection.prototype.fromLatLngToPoint = function(latLng) { + var x = latLng.lng() * overviewerConfig.CONST.tileSize; + var y = latLng.lat() * overviewerConfig.CONST.tileSize; + return new google.maps.Point(x, y); + }; + + overviewer.classes.MapProjection.prototype.fromPointToLatLng = function(point) { + var lng = point.x * this.inverseTileSize; + var lat = point.y * this.inverseTileSize; + return new google.maps.LatLng(lat, lng); + }; + + overviewer.classes.CoordMapType.prototype.getTile = function(coord, zoom, ownerDocument) { + var div = ownerDocument.createElement('DIV'); + div.innerHTML = '(' + coord.x + ', ' + coord.y + ', ' + zoom + + ')' + '
'; + //TODO: figure out how to get the current mapType, I think this + //will add the maptile url to the grid thing once it works + + //div.innerHTML += overviewer.collections.mapTypes[0].getTileUrl(coord, zoom); + + //this should probably just have a css class + div.style.width = this.tileSize.width + 'px'; + div.style.height = this.tileSize.height + 'px'; + div.style.fontSize = '10px'; + div.style.borderStyle = 'solid'; + div.style.borderWidth = '1px'; + div.style.borderColor = '#AAAAAA'; + return div; + }; + }, + /** + * Setup the varous mapTypes before we actually create the map. This used + * to be a bunch of crap down at the bottom of functions.js + */ + 'initializeMapTypes': function() { + var mapOptions = {}; + for (i in overviewerConfig.mapTypes) { + var view = overviewerConfig.mapTypes[i]; + var imageFormat = view.imgformat ? view.imgformat : 'png'; + mapOptions[view.label] = { + 'getTileUrl': overviewer.gmap.getTileUrlGenerator(view.path, + view.base, imageFormat), + 'tileSize': new google.maps.Size( + overviewerConfig.CONST.tileSize, + overviewerConfig.CONST.tileSize), + 'maxZoom': overviewerConfig.map.maxZoom, + 'minZoom': overviewerConfig.map.minZoom, + 'isPng': imageFormat.toLowerCase() == 'png' + } + overviewer.collections.mapTypes[view.label] = new google.maps.ImageMapType( + mapOptions[view.label]); + overviewer.collections.mapTypes[view.label].name = view.label; + overviewer.collections.mapTypes[view.label].alt = 'Minecraft ' + + view.label + ' Map'; + overviewer.collections.mapTypes[view.label].projection = + new overviewer.classes.MapProjection(); + if (view.overlay) { + overviewer.collections.overlays.push( + overviewer.collections.mapTypes[view.label]); + } else { + overviewer.collections.mapTypeIds.push( + overviewerConfig.CONST.mapDivId + view.label); + } + } + }, + /** + * This is where the magic happens. We setup the map with all it's + * options. The query string is also parsed here so we can know if + * we should be looking at a particular point on the map or just use + * the default view. + */ + 'initializeMap': function() { + var defaultCenter = overviewer.util.fromWorldToLatLng( + overviewerConfig.map.center[0], overviewerConfig.map.center[1], + overviewerConfig.map.center[2]); + var lat = defaultCenter.lat(); + var lng = defaultCenter.lng(); + var zoom = overviewerConfig.map.defaultZoom; + var mapcenter; + queryParams = overviewer.util.parseQueryString(); + if (queryParams.lat) { + lat = parseFloat(queryParams.lat); + } + if (queryParams.lng) { + lng = parseFloat(queryParams.lng); + } + if (queryParams.zoom) { + if (queryParams.zoom == 'max') { + zoom = overviewerConfig.map.maxZoom; + } else if (queryParams.zoom == 'min') { + zoom = overviewerConfig.map.minZoom; + } else { + zoom = parseInt(queryParams.zoom); + if (zoom < 0 && zoom + overviewerConfig.map.maxZoom >= 0) { + //if zoom is negative, try to treat as "zoom out from max zoom" + zoom += overviewerConfig.map.maxZoom; + } else { + //fall back to default zoom + zoom = overviewerConfig.map.defaultZoom; + } + } + } + if (queryParams.x && queryParams.y && queryParams.z) { + mapcenter = overviewer.util.fromWorldToLatLng(queryParams.x, + queryParams.y, queryParams.z); + // Add a market indicating the user-supplied position + overviewer.collections.markerDatas.push([{ + 'msg': 'Coordinates ' + queryParams.x + ', ' + + queryParams.y + ', ' + queryParams.z, + 'y': parseFloat(queryParams.y), + 'x': parseFloat(queryParams.x), + 'z': parseFloat(queryParams.z), + 'type': 'querypos'}]); + } else { + mapcenter = new google.maps.LatLng(lat, lng); + } + var mapOptions = { + zoom: zoom, + center: mapcenter, + panControl: overviewerConfig.map.controls.pan, + scaleControl: false, + mapTypeControl: overviewerConfig.map.controls.mapType && + overviewer.collections.mapTypeIds.length > 1, + mapTypeControlOptions: { + mapTypeIds: overviewer.collections.mapTypeIds + }, + mapTypeId: overviewer.util.getDefaultMapTypeId(), + streetViewControl: false, + zoomControl: overviewerConfig.map.controls.zoom, + backgroundColor: overviewer.util.getMapTypeBackgroundColor( + overviewer.util.getDefaultMapTypeId()) + }; + overviewer.map = new google.maps.Map(document.getElementById( + overviewerConfig.CONST.mapDivId), mapOptions); + + if (overviewerConfig.map.debug) { + overviewer.map.overlayMapTypes.insertAt(0, + new overviewer.classes.CoordMapType(new google.maps.Size( + overviewerConfig.CONST.tileSize, + overviewerConfig.CONST.tileSize))); + google.maps.event.addListener(overviewer.map, 'click', function(event) { + overviewer.util.debug('latLng: (' + event.latLng.lat() + + ', ' + event.latLng.lng() + ')'); + var pnt = overviewer.map.getProjection().fromLatLngToPoint(event.latLng); + overviewer.util.debug('point: ' + pnt); + var pxx = pnt.x * overviewerConfig.CONST.tileSize * + Math.pow(2, overviewerConfig.map.maxZoom); + var pxy = pnt.y * overviewerConfig.CONST.tileSize * + Math.pow(2, overviewerConfig.map.maxZoom); + overviewer.util.debug('pixel: (' + pxx + ', ' + pxy + ')'); + }); + } + + // Now attach the coordinate map type to the map's registry + for (i in overviewer.collections.mapTypes) { + overviewer.map.mapTypes.set(overviewerConfig.CONST.mapDivId + + overviewer.collections.mapTypes[i].name, + overviewer.collections.mapTypes[i]); + } + + // Make the link again whenever the map changes + google.maps.event.addListener(overviewer.map, 'maptypeid_changed', function() { + $('#'+overviewerConfig.CONST.mapDivId).css( + 'background-color', overviewer.util.getMapTypeBackgroundColor( + overviewer.map.getMapTypeId())); + }); + // We can now set the map to use the 'coordinate' map type + overviewer.map.setMapTypeId(overviewer.util.getDefaultMapTypeId()); + }, + /** + * Read through overviewer.collections.markerDatas and create Marker + * objects and stick them in overviewer.collections.markers . This + * should probably be done differently at some point so that we can + * support markers that change position more easily. + */ + 'initializeMarkers': function() { + //first, give all collections an empty array to work with + for (i in overviewerConfig.objectGroups.signs) { + overviewer.util.debug('Found sign group: ' + + overviewerConfig.objectGroups.signs[i].label); + overviewer.collections.markers[ + overviewerConfig.objectGroups.signs[i].label] = []; + } + for (i in overviewer.collections.markerDatas) { + var markerData = overviewer.collections.markerDatas[i]; + for (j in markerData) { + var item = markerData[j]; + // a default: + var iconURL = ''; + if (item.type == 'spawn') { + // don't filter spawn, always display + var marker = new google.maps.Marker({ + 'position': overviewer.util.fromWorldToLatLng(item.x, + item.y, item.z), + 'map': overviewer.map, + 'title': jQuery.trim(item.msg), + 'icon': overviewerConfig.CONST.image.spawnMarker + }); + continue; + } + + if (item.type == 'querypos') { + // Set on page load if MC x/y/z coords are given in the + // query string + var marker = new google.maps.Marker({ + 'position': overviewer.util.fromWorldToLatLng(item.x, + item.y, item.z), + 'map': overviewer.map, + 'title': jQuery.trim(item.msg), + 'icon': overviewerConfig.CONST.image.queryMarker + }); + continue; + } + + var matched = false; + for (j in overviewerConfig.objectGroups.signs) { + var signGroup = overviewerConfig.objectGroups.signs[j]; + var label = signGroup.label; + if (signGroup.match(item)) { + matched = true; + // can add custom types of images for externally defined + // item types, like 'command' here. + if (item.type == 'sign') { + iconURL = overviewerConfig.CONST.image.signMarker; + } + overviewer.util.debug('Sign icon: ' + signGroup.icon); + if (signGroup.icon) { + iconURL = signGroup.icon; + } + var marker = new google.maps.Marker({ + 'position': overviewer.util.fromWorldToLatLng(item.x, + item.y, item.z), + 'map': overviewer.map, + 'title': jQuery.trim(item.msg), + 'icon': iconURL, + 'visible': false + }); + overviewer.util.debug(label); + overviewer.collections.markers[label].push(marker); + if (item.type == 'sign') { + overviewer.util.createMarkerInfoWindow(marker); + } + } + } + + if (!matched) { + // is this signpost doesn't match any of the groups in + // config.js, add it automatically to the "__others__" group + if (item.type == 'sign') { + iconURL = overviewerConfig.CONST.image.signMarker; + } + var marker = new google.maps.Marker({ + 'position': overviewer.util.fromWorldToLatLng(item.x, + item.y, item.z), + 'map': overviewer.map, + 'title': jQuery.trim(item.msg), + 'icon': iconURL, + 'visible': false + }); + if (overviewer.collections.markers['__others__']) { + overviewer.collections.markers['__others__'].push(marker); + } else { + overviewer.collections.markers['__others__'] = [marker]; + } + if (item.type == 'sign') { + overviewer.util.createMarkerInfoWindow(marker, item); + } + } + } + } + }, + /** + * Same as initializeMarkers() for the most part. + */ + 'initializeRegions': function() { + for (i in overviewerConfig.objectGroups.regions) { + overviewer.collections.regions[overviewerConfig.objectGroups.regions[i].label] = []; + } + for (i in overviewer.collections.regionDatas) { + var regionData = overviewer.collections.regionDatas[i]; + for (j in regionData) { + var region = regionData[j]; + // pull all the points out of the regions file. + var converted = new google.maps.MVCArray(); + for (k in region.path) { + var point = region.path[k]; + converted.push(overviewer.util.fromWorldToLatLng( + point.x, point.y, point.z)); + + } + for (k in overviewerConfig.objectGroups.regions) { + var regionGroup = overviewerConfig.objectGroups.regions[k]; + var clickable = regionGroup.clickable; + var label = regionGroup.label; + + if(region.label) { + var name = region.label + } else { + var name = 'rawr'; + clickable = false; // if it doesn't have a name, we dont have to show it. + } + + if(region.opacity) { + var strokeOpacity = region.opacity; + var fillOpacity = region.opacity * 0.25; + } else { + var strokeOpacity = region.strokeOpacity; + var fillOpacity = region.fillOpacity; + } + + if (region.closed) { + var shape = new google.maps.Polygon({ + 'name': name, + 'clickable': clickable, + 'geodesic': false, + 'map': null, + 'strokeColor': region.color, + 'strokeOpacity': strokeOpacity, + 'strokeWeight': overviewerConfig.CONST.regionStrokeWeight, + 'fillColor': region.color, + 'fillOpacity': fillOpacity, + 'zIndex': j, + 'paths': converted + }); + } else { + var shape = new google.maps.Polyline({ + 'name': name, + 'clickable': clickable, + 'geodesic': false, + 'map': null, + 'strokeColor': region.color, + 'strokeOpacity': strokeOpacity, + 'strokeWeight': overviewerConfig.CONST.regionStrokeWeight, + 'zIndex': j, + 'path': converted + }); + } + overviewer.collections.regions[label].push(shape); + + if (clickable) { + overviewer.util.createRegionInfoWindow(shape); + } + } + } + } + }, + /** + * Change the map's div's background color according to the mapType's bg_color setting + * + * @param string mapTypeId + * @return string + */ + 'getMapTypeBackgroundColor': function(mapTypeId) { + for(i in overviewerConfig.mapTypes) { + if( overviewerConfig.CONST.mapDivId + + overviewerConfig.mapTypes[i].label == mapTypeId ) { + overviewer.util.debug('Found background color for: ' + + overviewerConfig.mapTypes[i].bg_color); + return overviewerConfig.mapTypes[i].bg_color; + } + } + }, + /** + * Gee, I wonder what this does. + * + * @param string msg + */ + 'debug': function(msg) { + if (overviewerConfig.map.debug) { + console.log(msg); + } + }, + /** + * Simple helper function to split the query string into key/value + * pairs. Doesn't do any type conversion but both are lowercase'd. + * + * @return Object + */ + 'parseQueryString': function() { + var results = {}; + var queryString = location.search.substring(1); + var pairs = queryString.split('&'); + for (i in pairs) { + var pos = pairs[i].indexOf('='); + var key = pairs[i].substring(0,pos).toLowerCase(); + var value = pairs[i].substring(pos+1).toLowerCase(); + overviewer.util.debug( 'Found GET paramter: ' + key + ' = ' + value); + results[key] = value; + } + return results; + }, + /** + * Set the link (at the bottom of the screen) to the current view. + * TODO: make this preserve the mapTypeId as well + */ + 'setViewUrl': function() { + var displayZoom = overviewer.map.getZoom(); + if (displayZoom == overviewerConfig.map.maxZoom) { + displayZoom = 'max'; + } else { + displayZoom -= overviewerConfig.map.maxZoom; + } + var point; + var point = overviewer.util.fromLatLngToWorld( + overviewer.map.getCenter().lat(), overviewer.map.getCenter().lng()); + var viewUrl = location.href.substring(0, location.href.lastIndexOf( + location.search)) + + '?x=' + Math.floor(point.x) + + '&y=' + Math.floor(point.y) + + '&z=' + Math.floor(point.z) + + '&zoom=' + displayZoom; + document.getElementById('link').innerHTML = viewUrl; + + }, + 'getDefaultMapTypeId': function() { + return overviewer.collections.mapTypeIds[0]; + }, + /** + * helper to get map LatLng from world coordinates takes arguments in + * X, Y, Z order (arguments are *out of order*, because within the + * function we use the axes like the rest of Minecraft Overviewer -- + * with the Z and Y flipped from normal minecraft usage.) + * + * @param int x + * @param int z + * @param int y + * + * @return google.maps.LatLng + */ + 'fromWorldToLatLng': function(x, z, y) { + // the width and height of all the highest-zoom tiles combined, + // inverted + var perPixel = 1.0 / (overviewerConfig.CONST.tileSize * + Math.pow(2, overviewerConfig.map.maxZoom)); + + // This information about where the center column is may change with + // a different drawing implementation -- check it again after any + // drawing overhauls! + + // point (0, 0, 127) is at (0.5, 0.0) of tile (tiles/2 - 1, tiles/2) + // so the Y coordinate is at 0.5, and the X is at 0.5 - + // ((tileSize / 2) / (tileSize * 2^maxZoom)) + // or equivalently, 0.5 - (1 / 2^(maxZoom + 1)) + var lng = 0.5 - (1.0 / Math.pow(2, overviewerConfig.map.maxZoom + 1)); + var lat = 0.5; + + // the following metrics mimic those in ChunkRenderer.chunk_render + // in "chunk.py" or, equivalently, chunk_render in src/iterate.c + + // each block on X axis adds 12px to x and subtracts 6px from y + lng += 12 * x * perPixel; + lat -= 6 * x * perPixel; + + // each block on Y axis adds 12px to x and adds 6px to y + lng += 12 * y * perPixel; + lat += 6 * y * perPixel; + + // each block down along Z adds 12px to y + lat += 12 * (128 - z) * perPixel; + + // add on 12 px to the X coordinate to center our point + lng += 12 * perPixel; + + return new google.maps.LatLng(lat, lng); + }, + /** + * The opposite of fromWorldToLatLng + * NOTE: X, Y and Z in this function are Minecraft world definitions + * (that is, X is horizontal, Y is altitude and Z is vertical). + * + * @param float lat + * @param float lng + * + * @return Array + */ + 'fromLatLngToWorld': function(lat, lng) { + // Initialize world x/y/z object to be returned + var point = Array(); + point.x = 0; + point.y = 64; + point.z = 0; + + // the width and height of all the highest-zoom tiles combined, + // inverted + var perPixel = 1.0 / (overviewerConfig.CONST.tileSize * + Math.pow(2, overviewerConfig.map.maxZoom)); + + // Revert base positioning + // See equivalent code in fromWorldToLatLng() + lng -= 0.5 - (1.0 / Math.pow(2, overviewerConfig.map.maxZoom + 1)); + lat -= 0.5; + + // I'll admit, I plugged this into Wolfram Alpha: + // a = (x * 12 * r) + (z * 12 * r), b = (z * 6 * r) - (x * 6 * r) + // And I don't know the math behind solving for for X and Z given + // A (lng) and B (lat). But Wolfram Alpha did. :) I'd welcome + // suggestions for splitting this up into long form and documenting + // it. -RF + point.x = (lng - 2 * lat) / (24 * perPixel) + point.z = (lng + 2 * lat) / (24 * perPixel) + + // Adjust for the fact that we we can't figure out what Y is given + // only latitude and longitude, so assume Y=64. + point.x += 64 + 1; + point.z -= 64 + 2; + + return point; + }, + /** + * Create and draw the various map controls and other related things + * like the compass, current view link, etc. + */ + 'createMapControls': function() { + // viewstate link (little link to where you're looking at the map, + // normally bottom left) + var viewStateDiv = document.createElement('DIV'); + viewStateDiv.id='link'; + // add it to the map, bottom left. + if (overviewerConfig.map.controls.link) { + google.maps.event.addListener(overviewer.map, 'zoom_changed', function() { + overviewer.util.setViewUrl(); + }); + google.maps.event.addListener(overviewer.map, 'center_changed', function() { + overviewer.util.setViewUrl(); + }); + overviewer.map.controls[google.maps.ControlPosition.BOTTOM_LEFT].push(viewStateDiv); + } + + // compass rose, in the top right corner + var compassDiv = document.createElement('DIV'); + compassDiv.style.padding = '5px'; + var compassImg = document.createElement('IMG'); + compassImg.src = overviewerConfig.CONST.image.compass; + compassDiv.appendChild(compassImg); + compassDiv.index = 0; + // add it to the map, top right. + if (overviewerConfig.map.controls.compass) { + overviewer.map.controls[google.maps.ControlPosition.TOP_RIGHT].push(compassDiv); + } + + // Spawn button + var homeControlDiv = document.createElement('DIV'); + var homeControl = new overviewer.classes.HomeControl(homeControlDiv); + homeControlDiv.id = 'customControl'; + homeControlDiv.index = 1; + if (overviewerConfig.map.controls.spawn) { + overviewer.map.controls[google.maps.ControlPosition.TOP_RIGHT].push(homeControlDiv); + } + + // only need to create the control if there are items in the list. + // as defined in config.js + if (overviewerConfig.objectGroups.signs.length > 0) { + // signpost display control + var items = []; + for (i in overviewerConfig.objectGroups.signs) { + var signGroup = overviewerConfig.objectGroups.signs[i]; + var iconURL = signGroup.icon; + if(!iconURL) { + iconURL = overviewerConfig.CONST.image.defaultMarker; + } + items.push({ + 'label': signGroup.label, + 'checked': signGroup.checked, + 'icon': iconURL, + 'action': function(n, item, checked) { + jQuery.each(overviewer.collections.markers[item.label], + function(i, elem) { + elem.setVisible(checked); + } + ); + overviewer.util.debug('Adding sign item: ' + item); + } + }); + } + overviewer.util.createDropDown('Signposts', items); + } + + // if there are any regions data, lets show the option to hide/show them. + if (overviewerConfig.objectGroups.regions.length > 0) { + // region display control + var items = []; + for (i in overviewerConfig.objectGroups.regions) { + var regionGroup = overviewerConfig.objectGroups.regions[i]; + items.push({ + 'label': regionGroup.label, + 'checked': regionGroup.checked, + 'action': function(n, item, checked) { + jQuery.each(overviewer.collections.regions[item.label], + function(i,elem) { + // Thanks to LeastWeasel for this line! + elem.setMap(checked ? overviewer.map : null); + }); + overviewer.util.debug('Adding region item: ' + item); + + } + }); + } + overviewer.util.createDropDown('Regions', items); + } + + if (overviewer.collections.overlays.length > 0) { + // overlay maps control + var items = []; + for (i in overviewer.collections.overlays) { + var overlay = overviewer.collections.overlays[i]; + items.push({ + 'label': overlay.name, + 'checked': false, + 'overlay': overlay, + 'action': function(i, item, checked) { + if (checked) { + overviewer.map.overlayMapTypes.push(item.overlay); + } else { + var idx_to_delete = -1; + overviewer.map.overlayMapTypes.forEach(function(e, j) { + if (e == item.overlay) { + idx_to_delete = j; + } + }); + if (idx_to_delete >= 0) { + overviewer.map.overlayMapTypes.removeAt(idx_to_delete); + } + } + } + }); + } + overviewer.util.createDropDown('Overlays', items); + } + }, + /** + * Reusable method for creating drop-down menus + * + * @param string title + * @param array items + */ + 'createDropDown': function(title, items) { + var control = document.createElement('DIV'); + // let's let a style sheet do most of the styling here + control.id = 'customControl'; + + var controlText = document.createElement('DIV'); + controlText.innerHTML = title; + + var controlBorder = document.createElement('DIV'); + controlBorder.id='top'; + control.appendChild(controlBorder); + controlBorder.appendChild(controlText); + + var dropdownDiv = document.createElement('DIV'); + dropdownDiv.id='dropDown'; + control.appendChild(dropdownDiv); + dropdownDiv.innerHTML=''; + + // add the functionality to toggle visibility of the items + $(controlText).click(function() { + $(dropdownDiv).toggle(); + }); + + // add that control box we've made back to the map. + overviewer.map.controls[google.maps.ControlPosition.TOP_RIGHT].push(control); + + for(i in items) { + // create the visible elements of the item + var item = items[i]; + overviewer.util.debug(item); + var itemDiv = document.createElement('div'); + var itemInput = document.createElement('input'); + itemInput.type='checkbox'; + + // give it a name + $(itemInput).data('label',item.label); + jQuery(itemInput).click((function(local_idx, local_item) { + return function(e) { + item.action(local_idx, local_item, e.target.checked); + }; + })(i, item)); + + // if its checked, its gotta do something, do that here. + if (item.checked) { + itemInput.checked = true; + item.action(i, item, item.checked); + } + dropdownDiv.appendChild(itemDiv); + itemDiv.appendChild(itemInput); + var textNode = document.createElement('text'); + if(item.icon) { + textNode.innerHTML = '' + item.label + '
'; + } else { + textNode.innerHTML = item.label + '
'; + } + + itemDiv.appendChild(textNode); + } + }, + /** + * Create the pop-up infobox for when you click on a region, this can't + * be done in-line because of stupid Javascript scoping problems with + * closures or something. + * + * @param google.maps.Polygon|google.maps.Polyline shape + */ + 'createRegionInfoWindow': function(shape) { + var infowindow = new google.maps.InfoWindow(); + google.maps.event.addListener(shape, 'click', function(event, i) { + if (overviewer.collections.infoWindow) { + overviewer.collections.infoWindow.close(); + } + // Replace our Info Window's content and position + var contentString = 'Region: ' + shape.name + '
' + + 'Clicked Location:
' + event.latLng.lat() + ', ' + + event.latLng.lng() + '
'; + infowindow.setContent(contentString); + infowindow.setPosition(event.latLng); + infowindow.open(overviewer.map); + overviewer.collections.infoWindow = infowindow; + }); + }, + /** + * Same as createRegionInfoWindow() + * + * @param google.maps.Marker marker + */ + 'createMarkerInfoWindow': function(marker) { + var windowContent = '

' + marker.title.replace(/\n/g,'
') + '

'; + var infowindow = new google.maps.InfoWindow({ + 'content': windowContent + }); + google.maps.event.addListener(marker, 'click', function() { + if (overviewer.collections.infoWindow) { + overviewer.collections.infoWindow.close(); + } + infowindow.open(overviewer.map, marker); + overviewer.collections.infoWindow = infowindow; + }); + } + }, + /** + * The various classes needed in this file. + */ + 'classes': { + /** + * This is the button that centers the map on spawn. Not sure why we + * need a separate class for this and not some of the other controls. + * + * @param documentElement controlDiv + */ + 'HomeControl': function(controlDiv) { + controlDiv.style.padding = '5px'; + // Set CSS for the control border + var control = document.createElement('DIV'); + control.id='top'; + control.title = 'Click to center the map on the Spawn'; + controlDiv.appendChild(control); + + // Set CSS for the control interior + var controlText = document.createElement('DIV'); + controlText.innerHTML = 'Spawn'; + controlText.id='button'; + control.appendChild(controlText); + + // Setup the click event listeners: simply set the map to map center + // as definned below + google.maps.event.addDomListener(control, 'click', function() { + overviewer.map.panTo(overviewer.util.fromWorldToLatLng( + overviewerConfig.map.center[0], + overviewerConfig.map.center[1], + overviewerConfig.map.center[2])); + }); + }, + /** + * Our custom projection maps Latitude to Y, and Longitude to X as + * normal, but it maps the range [0.0, 1.0] to [0, tileSize] in both + * directions so it is easier to position markers, etc. based on their + * position (find their position in the lowest-zoom image, and divide + * by tileSize) + */ + 'MapProjection' : function() { + this.inverseTileSize = 1.0 / overviewerConfig.CONST.tileSize; + }, + /** + * This is a mapType used only for debugging, to draw a grid on the screen + * showing the tile co-ordinates and tile path. Currently the tile path + * part does not work. + * + * @param google.maps.Size tileSize + */ + 'CoordMapType': function(tileSize) { + this.tileSize = tileSize; + } + }, + /** + * Stuff that we give to the google maps code instead of using ourselves + * goes in here. + * + * Also, why do I keep writing these comments as if I'm multiple people? I + * should probably stop that. + */ + 'gmap': { + /** + * Generate a function to get the path to a tile at a particular location + * and zoom level. + * + * @param string path + * @param string pathBase + * @param string pathExt + */ + 'getTileUrlGenerator': function(path, pathBase, pathExt) { + return function(tile, zoom) { + var url = path; + var urlBase = ( pathBase ? pathBase : '' ); + if(tile.x < 0 || tile.x >= Math.pow(2, zoom) || + tile.y < 0 || tile.y >= Math.pow(2, zoom)) { + url += '/blank'; + } else if(zoom == 0) { + url += '/base'; + } else { + for(var z = zoom - 1; z >= 0; --z) { + var x = Math.floor(tile.x / Math.pow(2, z)) % 2; + var y = Math.floor(tile.y / Math.pow(2, z)) % 2; + url += '/' + (x + 2 * y); + } + } + url = url + '.' + pathExt; + if(overviewerConfig.map.cacheMinutes > 0) { + var d = new Date(); + url += '?c=' + Math.floor(d.getTime() / + (1000 * 60 * overviewerConfig.map.cacheMinutes)); + } + return(urlBase + url); + } + } + } +}; diff --git a/overviewer_core/googlemap.py b/overviewer_core/googlemap.py index f6f271e..af56fbe 100644 --- a/overviewer_core/googlemap.py +++ b/overviewer_core/googlemap.py @@ -23,6 +23,7 @@ from time import strftime, gmtime import json import util +from c_overviewer import get_render_mode_inheritance """ This module has routines related to generating a Google Maps-based @@ -59,25 +60,25 @@ def mirror_dir(src, dst, entities=None): # if this stills throws an error, let it propagate up class MapGen(object): - def __init__(self, quadtrees, skipjs=False, web_assets_hook=None): + def __init__(self, quadtrees, configInfo): """Generates a Google Maps interface for the given list of quadtrees. All of the quadtrees must have the same destdir, image format, and world. Note:tiledir for each quadtree should be unique. By default the tiledir is determined by the rendermode""" - self.skipjs = skipjs - self.web_assets_hook = web_assets_hook + self.skipjs = configInfo.get('skipjs', None) + self.web_assets_hook = configInfo.get('web_assets_hook', None) + self.bg_color = configInfo.get('bg_color') if not len(quadtrees) > 0: raise ValueError("there must be at least one quadtree to work on") self.destdir = quadtrees[0].destdir - self.imgformat = quadtrees[0].imgformat self.world = quadtrees[0].world self.p = quadtrees[0].p for i in quadtrees: - if i.destdir != self.destdir or i.imgformat != self.imgformat or i.world != self.world: - raise ValueError("all the given quadtrees must have the same destdir") + if i.destdir != self.destdir or i.world != self.world: + raise ValueError("all the given quadtrees must have the same destdir and world") self.quadtrees = quadtrees @@ -85,32 +86,38 @@ class MapGen(object): """Writes out config.js, marker.js, and region.js Copies web assets into the destdir""" zoomlevel = self.p - imgformat = self.imgformat - configpath = os.path.join(util.get_program_path(), "config.js") + configpath = os.path.join(util.get_program_path(), "overviewerConfig.js") config = open(configpath, 'r').read() config = config.replace( - "{maxzoom}", str(zoomlevel)) + "{minzoom}", str(0)) config = config.replace( - "{imgformat}", str(imgformat)) + "{maxzoom}", str(zoomlevel)) config = config.replace("{spawn_coords}", json.dumps(list(self.world.spawn))) + + #config = config.replace("{bg_color}", self.bg_color) # create generated map type data, from given quadtrees maptypedata = map(lambda q: {'label' : q.rendermode.capitalize(), - 'path' : q.tiledir}, self.quadtrees) + 'path' : q.tiledir, + 'bg_color': self.bg_color, + 'overlay' : 'overlay' in get_render_mode_inheritance(q.rendermode), + 'imgformat' : q.imgformat}, + self.quadtrees) config = config.replace("{maptypedata}", json.dumps(maptypedata)) - with open(os.path.join(self.destdir, "config.js"), 'w') as output: + with open(os.path.join(self.destdir, "overviewerConfig.js"), 'w') as output: output.write(config) + bgcolor = (int(self.bg_color[1:3],16), int(self.bg_color[3:5],16), int(self.bg_color[5:7],16), 0) + blank = Image.new("RGBA", (1,1), bgcolor) # Write a blank image for quadtree in self.quadtrees: - blank = Image.new("RGBA", (1,1)) tileDir = os.path.join(self.destdir, quadtree.tiledir) if not os.path.exists(tileDir): os.mkdir(tileDir) - blank.save(os.path.join(tileDir, "blank."+self.imgformat)) + blank.save(os.path.join(tileDir, "blank."+quadtree.imgformat)) # copy web assets into destdir: mirror_dir(os.path.join(util.get_program_path(), "web_assets"), self.destdir) @@ -144,13 +151,13 @@ class MapGen(object): # write out the default marker table with open(os.path.join(self.destdir, "markers.js"), 'w') as output: - output.write("var markerData=[\n") + output.write("overviewer.collections.markerDatas.push([\n") for marker in self.world.POI: output.write(json.dumps(marker)) if marker != self.world.POI[-1]: output.write(",") output.write("\n") - output.write("]\n") + output.write("]);\n") # save persistent data self.world.persistentData['POI'] = self.world.POI @@ -159,11 +166,11 @@ class MapGen(object): # write out the default (empty, but documented) region table with open(os.path.join(self.destdir, "regions.js"), 'w') as output: - output.write('var regionData=[\n') + output.write('overviewer.collections.regionDatas.push([\n') output.write(' // {"color": "#FFAA00", "opacity": 0.5, "closed": true, "path": [\n') output.write(' // {"x": 0, "y": 0, "z": 0},\n') output.write(' // {"x": 0, "y": 10, "z": 0},\n') output.write(' // {"x": 0, "y": 0, "z": 10}\n') output.write(' // ]},\n') - output.write('];') + output.write(']);') diff --git a/overviewer_core/quadtree.py b/overviewer_core/quadtree.py index d4018f6..688db93 100644 --- a/overviewer_core/quadtree.py +++ b/overviewer_core/quadtree.py @@ -34,6 +34,7 @@ from PIL import Image import nbt import chunk +from c_overviewer import get_render_mode_inheritance from optimizeimages import optimize_image import composite @@ -48,7 +49,7 @@ def iterate_base4(d): return itertools.product(xrange(4), repeat=d) class QuadtreeGen(object): - def __init__(self, worldobj, destdir, depth=None, tiledir=None, imgformat=None, optimizeimg=None, rendermode="normal"): + def __init__(self, worldobj, destdir, bgcolor, depth=None, tiledir=None, imgformat=None, imgquality=95, optimizeimg=None, rendermode="normal"): """Generates a quadtree from the world given into the given dest directory @@ -60,16 +61,18 @@ class QuadtreeGen(object): """ assert(imgformat) self.imgformat = imgformat + self.imgquality = imgquality self.optimizeimg = optimizeimg - - self.lighting = rendermode in ("lighting", "night", "spawn") - self.night = rendermode in ("night", "spawn") - self.spawn = rendermode in ("spawn",) + self.bgcolor = bgcolor self.rendermode = rendermode + + # force png renderformat if we're using an overlay mode + if 'overlay' in get_render_mode_inheritance(rendermode): + self.imgformat = "png" # Make the destination dir if not os.path.exists(destdir): - os.mkdir(destdir) + os.makedirs(os.path.abspath(destdir)) if tiledir is None: tiledir = rendermode self.tiledir = tiledir @@ -89,7 +92,7 @@ class QuadtreeGen(object): yradius >= worldobj.maxrow and -yradius <= worldobj.minrow: break else: - raise ValueError("Your map is waaaay too big! Use the '-z' or '--zoom' options.") + raise ValueError("Your map is waaaay too big! Use the 'zoom' option in 'settings.py'.") self.p = p else: @@ -113,10 +116,10 @@ class QuadtreeGen(object): returns -1 if it couldn't be detected, file not found, or nothing in config.js matched """ - indexfile = os.path.join(self.destdir, "config.js") + indexfile = os.path.join(self.destdir, "overviewerConfig.js") if not os.path.exists(indexfile): return -1 - matcher = re.compile(r"maxZoom:\s*(\d+)") + matcher = re.compile(r"maxZoom.*:\s*(\d+)") p = -1 for line in open(indexfile, "r"): res = matcher.search(line) @@ -320,7 +323,7 @@ class QuadtreeGen(object): #logging.debug("writing out innertile {0}".format(imgpath)) # Create the actual image now - img = Image.new("RGBA", (384, 384), (38,92,255,0)) + img = Image.new("RGBA", (384, 384), self.bgcolor) # we'll use paste (NOT alpha_over) for quadtree generation because # this is just straight image stitching, not alpha blending @@ -334,7 +337,7 @@ class QuadtreeGen(object): # Save it if self.imgformat == 'jpg': - img.save(imgpath, quality=95, subsampling=0) + img.save(imgpath, quality=self.imgquality, subsampling=0) else: # png img.save(imgpath) @@ -442,7 +445,7 @@ class QuadtreeGen(object): #logging.debug("writing out worldtile {0}".format(imgpath)) # Compile this image - tileimg = Image.new("RGBA", (width, height), (38,92,255,0)) + tileimg = Image.new("RGBA", (width, height), self.bgcolor) world = self.world rendermode = self.rendermode @@ -461,7 +464,10 @@ class QuadtreeGen(object): pass # Save them - tileimg.save(imgpath) + if self.imgformat == 'jpg': + tileimg.save(imgpath, quality=self.imgquality, subsampling=0) + else: # png + tileimg.save(imgpath) if self.optimizeimg: optimize_image(imgpath, self.imgformat, self.optimizeimg) diff --git a/overviewer_core/rendernode.py b/overviewer_core/rendernode.py index d762b73..9dce5cc 100644 --- a/overviewer_core/rendernode.py +++ b/overviewer_core/rendernode.py @@ -149,7 +149,6 @@ class RenderNode(object): total += 4**q.p if q.p > max_p: max_p = q.p - q.go(procs) self.max_p = max_p # Render the highest level of tiles from the chunks results = collections.deque() diff --git a/overviewer_core/src/composite.c b/overviewer_core/src/composite.c index 4a03d23..432ece8 100644 --- a/overviewer_core/src/composite.c +++ b/overviewer_core/src/composite.c @@ -273,7 +273,8 @@ alpha_over_wrap(PyObject *self, PyObject *args) * also, it multiplies instead of doing an over operation */ PyObject * -tint_with_mask(PyObject *dest, unsigned char sr, unsigned char sg, unsigned char sb, +tint_with_mask(PyObject *dest, unsigned char sr, unsigned char sg, + unsigned char sb, unsigned char sa, PyObject *mask, int dx, int dy, int xsize, int ysize) { /* libImaging handles */ Imaging imDest, imMask; @@ -332,9 +333,11 @@ tint_with_mask(PyObject *dest, unsigned char sr, unsigned char sg, unsigned char out++; *out = MULDIV255(*out, sb, tmp1); out++; + *out = MULDIV255(*out, sa, tmp1); + out++; } else if (*inmask == 0) { /* do nothing -- source is fully transparent */ - out += 3; + out += 4; } else { /* general case */ @@ -345,9 +348,10 @@ tint_with_mask(PyObject *dest, unsigned char sr, unsigned char sg, unsigned char out++; *out = MULDIV255(*out, (255 - *inmask) + MULDIV255(sb, *inmask, tmp1), tmp2); out++; + *out = MULDIV255(*out, (255 - *inmask) + MULDIV255(sa, *inmask, tmp1), tmp2); + out++; } - out++; inmask += mask_stride; } } diff --git a/overviewer_core/src/iterate.c b/overviewer_core/src/iterate.c index 6f944de..f971ac0 100644 --- a/overviewer_core/src/iterate.c +++ b/overviewer_core/src/iterate.c @@ -155,7 +155,12 @@ generate_pseudo_data(RenderState *state, unsigned char ancilData) { int x = state->x, y = state->y, z = state->z; unsigned char data = 0; - if (state->block == 9) { /* water */ + if (state->block == 2) { /* grass */ + /* return 0x10 if grass is covered in snow */ + if (z < 127 && getArrayByte3D(state->blocks, x, y, z+1) == 78) + return 0x10; + return ancilData; + } else if (state->block == 9) { /* water */ /* an aditional bit for top is added to the 4 bits of check_adjacent_blocks */ if ((ancilData == 0) || (ancilData >= 10)) { /* static water, only top, and unkown ancildata values */ data = 16; @@ -207,9 +212,75 @@ generate_pseudo_data(RenderState *state, unsigned char ancilData) { if ((above_level_data & 0x08)) { /* draw top right going up redstonewire */ final_data = final_data | 0x10; } - return final_data; + return final_data; + + } else if (state-> block == 54) { /* chests */ + /* the top 2 bits are used to store the type of chest + * (single or double), the 2 bottom bits are used for + * orientation, look textures.py for more information. */ + + /* if placed alone chests always face west, return 0 to make a + * chest facing west */ + unsigned char chest_data = 0, air_data = 0, final_data = 0; + + /* search for chests */ + chest_data = check_adjacent_blocks(state, x, y, z, 54); + + /* search for air */ + air_data = check_adjacent_blocks(state, x, y, z, 0); + + if (chest_data == 1) { /* another chest in the east */ + final_data = final_data | 0x8; /* only can face to north or south */ + if ( (air_data & 0x2) == 2 ) { + final_data = final_data | 0x1; /* facing north */ + } else { + final_data = final_data | 0x3; /* facing south */ + } + + } else if (chest_data == 2) { /* in the north */ + final_data = final_data | 0x4; /* only can face to east or west */ + if ( !((air_data & 0x4) == 4) ) { /* 0 = west */ + final_data = final_data | 0x2; /* facing east */ + } + + } else if (chest_data == 4) { /*in the west */ + final_data = final_data | 0x4; + if ( (air_data & 0x2) == 2 ) { + final_data = final_data | 0x1; /* facing north */ + } else { + final_data = final_data | 0x3; /* facing south */ + } + + } else if (chest_data == 8) { /*in the south */ + final_data = final_data | 0x8; + if ( !((air_data & 0x4) == 4) ) { + final_data = final_data | 0x2; /* facing east */ + } + + } else if (chest_data == 0) { + /* Single chest, determine the orientation */ + if ( ((air_data & 0x8) == 0) && ((air_data & 0x2) == 2) ) { /* block in +x and no block in -x */ + final_data = final_data | 0x1; /* facing north */ + + } else if ( ((air_data & 0x2) == 0) && ((air_data & 0x8) == 8)) { + final_data = final_data | 0x3; + + } else if ( ((air_data & 0x4) == 0) && ((air_data & 0x1) == 1)) { + final_data = final_data | 0x2; + } /* else, facing west, value = 0 */ + + } else { + /* more than one adjacent chests! render as normal chest */ + return 0; + } + + return final_data; + + } else if (state->block == 90) { + return check_adjacent_blocks(state, x, y, z, state->block); } + return 0; } @@ -329,7 +400,7 @@ chunk_render(PyObject *self, PyObject *args) { PyObject *tmp; unsigned char ancilData = getArrayByte3D(blockdata_expanded, state.x, state.y, state.z); - if ((state.block == 85) || (state.block == 9) || (state.block == 55)) { + if ((state.block == 85) || (state.block == 9) || (state.block == 55) || (state.block == 54) || (state.block == 2) || (state.block == 90)) { ancilData = generate_pseudo_data(&state, ancilData); } diff --git a/overviewer_core/src/main.c b/overviewer_core/src/main.c index ddbe0af..4de7eec 100644 --- a/overviewer_core/src/main.c +++ b/overviewer_core/src/main.c @@ -25,14 +25,24 @@ PyObject *get_extension_version(PyObject *self, PyObject *args) { static PyMethodDef COverviewerMethods[] = { {"alpha_over", alpha_over_wrap, METH_VARARGS, "alpha over composite function"}, + {"render_loop", chunk_render, METH_VARARGS, "Renders stuffs"}, + {"get_render_modes", get_render_modes, METH_VARARGS, "returns available render modes"}, {"get_render_mode_info", get_render_mode_info, METH_VARARGS, "returns info for a particular render mode"}, + {"get_render_mode_parent", get_render_mode_parent, METH_VARARGS, + "returns parent for a particular render mode"}, + {"get_render_mode_inheritance", get_render_mode_inheritance, METH_VARARGS, + "returns inheritance chain for a particular render mode"}, + {"get_render_mode_children", get_render_mode_children, METH_VARARGS, + "returns (direct) children for a particular render mode"}, + {"extension_version", get_extension_version, METH_VARARGS, "Returns the extension version"}, + {NULL, NULL, 0, NULL} /* Sentinel */ }; diff --git a/overviewer_core/src/overviewer.h b/overviewer_core/src/overviewer.h index 6eac848..55243a5 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 3 +#define OVERVIEWER_EXTENSION_VERSION 5 /* Python PIL, and numpy headers */ #include @@ -48,7 +48,8 @@ PyObject *alpha_over(PyObject *dest, PyObject *src, PyObject *mask, PyObject *alpha_over_full(PyObject *dest, PyObject *src, PyObject *mask, float overall_alpha, int dx, int dy, int xsize, int ysize); PyObject *alpha_over_wrap(PyObject *self, PyObject *args); -PyObject *tint_with_mask(PyObject *dest, unsigned char sr, unsigned char sg, unsigned char sb, +PyObject *tint_with_mask(PyObject *dest, unsigned char sr, unsigned char sg, + unsigned char sb, unsigned char sa, PyObject *mask, int dx, int dy, int xsize, int ysize); /* in iterate.c */ diff --git a/overviewer_core/src/rendermode-cave.c b/overviewer_core/src/rendermode-cave.c new file mode 100644 index 0000000..f8334b2 --- /dev/null +++ b/overviewer_core/src/rendermode-cave.c @@ -0,0 +1,234 @@ +/* + * 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" +#include +//~ +//~ /* figures out the black_coeff from a given skylight and blocklight, used in + //~ lighting calculations -- note this is *different* from the one in + //~ rendermode-lighting.c (the "skylight - 11" part) */ +//~ static float calculate_darkness(unsigned char skylight, unsigned char blocklight) { + //~ return 1.0f - powf(0.8f, 15.0 - MAX(blocklight, skylight - 11)); +//~ } + +static int +rendermode_cave_occluded(void *data, RenderState *state) { + int x = state->x, y = state->y, z = state->z, dz = 0; + RenderModeCave* self; + self = (RenderModeCave *)data; + + /* check if the block is touching skylight */ + if (z != 127) { + + if (getArrayByte3D(self->skylight, x, y, z+1) != 0) { + return 1; + } + + if ((x == 15)) { + if (self->up_right_skylight != Py_None) { + if (getArrayByte3D(self->up_right_skylight, 0, y, z) != 0) { + return 1; + } + } + } else { + if (getArrayByte3D(self->skylight, x+1, y, z) != 0) { + return 1; + } + } + + if (x == 0) { + if (self->left_skylight != Py_None) { + if (getArrayByte3D(self->left_skylight, 15, y, z) != 0) { + return 1; + } + } + } else { + if (getArrayByte3D(self->skylight, x-1, y, z) != 0) { + return 1; + } + } + + if (y == 15) { + if (self->right_skylight != Py_None) { + if (getArrayByte3D(self->right_skylight, 0, y, z) != 0) { + return 1; + } + } + } else { + if (getArrayByte3D(self->skylight, x, y+1, z) != 0) { + return 1; + } + } + + if (y == 0) { + if (self->up_left_skylight != Py_None) { + if (getArrayByte3D(self->up_left_skylight, 15, y, z) != 0) { + return 1; + } + } + } else { + if (getArrayByte3D(self->skylight, x, y-1, z) != 0) { + return 1; + } + } + + /* check for normal occlusion */ + /* use ajacent chunks, if not you get blocks spreaded in chunk edges */ + if ( (x == 0) && (y != 15) ) { + if (state->left_blocks != Py_None) { + if (!is_transparent(getArrayByte3D(state->left_blocks, 15, y, z)) && + !is_transparent(getArrayByte3D(state->blocks, x, y, z+1)) && + !is_transparent(getArrayByte3D(state->blocks, x, y+1, z))) { + return 1; + } + } else { + return 1; + } + } + + if ( (x != 0) && (y == 15) ) { + if (state->right_blocks != Py_None) { + if (!is_transparent(getArrayByte3D(state->blocks, x-1, y, z)) && + !is_transparent(getArrayByte3D(state->right_blocks, x, 0, z)) && + !is_transparent(getArrayByte3D(state->blocks, x, y, z+1))) { + return 1; + } + } else { + return 1; + } + } + + if ( (x == 0) && (y == 15) ) { + if ((state->left_blocks != Py_None) && + (state->right_blocks != Py_None)) { + if (!is_transparent(getArrayByte3D(state->left_blocks, 15, y, z)) && + !is_transparent(getArrayByte3D(state->right_blocks, x, 0, z)) && + !is_transparent(getArrayByte3D(state->blocks, x, y, z+1))) { + return 1; + } + } else { + return 1; + } + } + + if ( (x != 0) && (y != 15) && + !is_transparent(getArrayByte3D(state->blocks, x-1, y, z)) && + !is_transparent(getArrayByte3D(state->blocks, x, y, z+1)) && + !is_transparent(getArrayByte3D(state->blocks, x, y+1, z))) { + return 1; + } + + } else { /* if z == 127 skip */ + return 1; + } + + /* check for lakes and seas and don't render them */ + /* at this point of the code the block has no skylight + * and is not occluded, but a deep sea can fool these + * 2 tests */ + + if ((getArrayByte3D(state->blocks, x, y, z) == 9) || + (getArrayByte3D(state->blocks, x, y, z+1) == 9)) { + + for (dz = z+1; dz < 127; dz++) { /* go up and check for skylight */ + if (getArrayByte3D(self->skylight, x, y, dz) != 0) { + return 1; + } + if (getArrayByte3D(state->blocks, x, y, dz) != 9) { + /* we are out of the water! and there's no skylight + * , i.e. is a cave lake or something similar */ + return 0; + } + } + } + + + return 0; +} + +static int +rendermode_cave_start(void *data, RenderState *state) { + RenderModeCave* self; + int ret; + self = (RenderModeCave *)data; + + /* first, chain up */ + ret = rendermode_normal.start(data, state); + if (ret != 0) + return ret; + + /* if there's skylight we are in the surface! */ + self->skylight = PyObject_GetAttrString(state->self, "skylight"); + self->left_skylight = PyObject_GetAttrString(state->self, "left_skylight"); + self->right_skylight = PyObject_GetAttrString(state->self, "right_skylight"); + self->up_left_skylight = PyObject_GetAttrString(state->self, "up_left_skylight"); + self->up_right_skylight = PyObject_GetAttrString(state->self, "up_right_skylight"); + + /* colors for tinting */ + self->depth_colors = PyObject_GetAttrString(state->chunk, "depth_colors"); + + + return 0; +} + +static void +rendermode_cave_finish(void *data, RenderState *state) { + RenderModeCave* self; + self = (RenderModeCave *)data; + + Py_DECREF(self->skylight); + Py_DECREF(self->left_skylight); + Py_DECREF(self->right_skylight); + Py_DECREF(self->up_left_skylight); + Py_DECREF(self->up_right_skylight); + + Py_DECREF(self->depth_colors); + + rendermode_normal.finish(data, state); +} + +static void +rendermode_cave_draw(void *data, RenderState *state, PyObject *src, PyObject *mask) { + RenderModeCave* self; + int z, r, g, b; + self = (RenderModeCave *)data; + + z = state->z; + r = 0, g = 0, b = 0; + + /* draw the normal block */ + rendermode_normal.draw(data, state, src, mask); + + /* get the colors and tint and tint */ + /* TODO TODO for a nether mode there isn't tinting! */ + r = PyInt_AsLong(PyList_GetItem(self->depth_colors, 0 + z*3)); + g = PyInt_AsLong(PyList_GetItem(self->depth_colors, 1 + z*3)); + b = PyInt_AsLong(PyList_GetItem(self->depth_colors, 2 + z*3)); + + tint_with_mask(state->img, r, g, b, 255, mask, state->imgx, state->imgy, 0, 0); + +} + +RenderModeInterface rendermode_cave = { + "cave", "render only caves in normal mode", + &rendermode_normal, + sizeof(RenderModeCave), + rendermode_cave_start, + rendermode_cave_finish, + rendermode_cave_occluded, + rendermode_cave_draw, +}; diff --git a/overviewer_core/src/rendermode-lighting.c b/overviewer_core/src/rendermode-lighting.c index 628e5c4..83b38d3 100644 --- a/overviewer_core/src/rendermode-lighting.c +++ b/overviewer_core/src/rendermode-lighting.c @@ -229,6 +229,7 @@ rendermode_lighting_draw(void *data, RenderState *state, PyObject *src, PyObject RenderModeInterface rendermode_lighting = { "lighting", "draw shadows from the lighting data", + &rendermode_normal, sizeof(RenderModeLighting), rendermode_lighting_start, rendermode_lighting_finish, diff --git a/overviewer_core/src/rendermode-night.c b/overviewer_core/src/rendermode-night.c index 1e54f05..46ca7c2 100644 --- a/overviewer_core/src/rendermode-night.c +++ b/overviewer_core/src/rendermode-night.c @@ -61,6 +61,7 @@ rendermode_night_draw(void *data, RenderState *state, PyObject *src, PyObject *m RenderModeInterface rendermode_night = { "night", "like \"lighting\", except at night", + &rendermode_lighting, sizeof(RenderModeNight), rendermode_night_start, rendermode_night_finish, diff --git a/overviewer_core/src/rendermode-normal.c b/overviewer_core/src/rendermode-normal.c index 1381eb0..b4118e3 100644 --- a/overviewer_core/src/rendermode-normal.c +++ b/overviewer_core/src/rendermode-normal.c @@ -120,17 +120,8 @@ rendermode_normal_draw(void *data, RenderState *state, PyObject *src, PyObject * RenderModeNormal *self = (RenderModeNormal *)data; /* first, check to see if we should use biome-compatible src, mask */ - if (self->biome_data) { - switch (state->block) { - case 2: - src = mask = self->grass_texture; - break; - case 18: - src = mask = self->leaf_texture; - break; - default: - break; - }; + if (self->biome_data && state->block == 18) { + src = mask = self->leaf_texture; } /* draw the block! */ @@ -147,9 +138,12 @@ rendermode_normal_draw(void *data, RenderState *state, PyObject *src, PyObject * switch (state->block) { case 2: - /* grass */ + /* grass -- skip for snowgrass */ + if (state->z < 127 && getArrayByte3D(state->blocks, state->x, state->y, state->z+1) == 78) + break; color = PySequence_GetItem(self->grasscolor, index); - facemask = self->facemask_top; + facemask = self->grass_texture; + alpha_over(state->img, self->grass_texture, self->grass_texture, state->imgx, state->imgy, 0, 0); break; case 18: /* leaves */ @@ -169,7 +163,7 @@ rendermode_normal_draw(void *data, RenderState *state, PyObject *src, PyObject * b = PyInt_AsLong(PyTuple_GET_ITEM(color, 2)); Py_DECREF(color); - tint_with_mask(state->img, r, g, b, facemask, state->imgx, state->imgy, 0, 0); + tint_with_mask(state->img, r, g, b, 255, facemask, state->imgx, state->imgy, 0, 0); } } @@ -183,7 +177,7 @@ rendermode_normal_draw(void *data, RenderState *state, PyObject *src, PyObject * int increment=0; if (state->block == 44) // half-step increment=6; - else if (state->block == 78) // snow + else if ((state->block == 78) || (state->block == 93) || (state->block == 94)) // snow, redstone repeaters (on and off) increment=9; if ((state->x == 15) && (state->up_right_blocks != Py_None)) { @@ -221,6 +215,7 @@ rendermode_normal_draw(void *data, RenderState *state, PyObject *src, PyObject * RenderModeInterface rendermode_normal = { "normal", "nothing special, just render the blocks", + NULL, sizeof(RenderModeNormal), rendermode_normal_start, rendermode_normal_finish, diff --git a/overviewer_core/src/rendermode-overlay.c b/overviewer_core/src/rendermode-overlay.c new file mode 100644 index 0000000..fb4e765 --- /dev/null +++ b/overviewer_core/src/rendermode-overlay.c @@ -0,0 +1,137 @@ +/* + * 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 void get_color(void *data, RenderState *state, + unsigned char *r, unsigned char *g, unsigned char *b, unsigned char *a) { + *r = 200; + *g = 200; + *b = 255; + *a = 155; +} + +static int +rendermode_overlay_start(void *data, RenderState *state) { + PyObject *facemasks_py; + RenderModeOverlay *self = (RenderModeOverlay *)data; + + facemasks_py = PyObject_GetAttrString(state->chunk, "facemasks"); + /* borrowed reference, needs to be incref'd if we keep it */ + self->facemask_top = PyTuple_GetItem(facemasks_py, 0); + Py_INCREF(self->facemask_top); + Py_DECREF(facemasks_py); + + self->white_color = PyObject_GetAttrString(state->chunk, "white_color"); + + self->solid_blocks = PyObject_GetAttrString(state->chunk, "solid_blocks"); + self->fluid_blocks = PyObject_GetAttrString(state->chunk, "fluid_blocks"); + + self->get_color = get_color; + + return 0; +} + +static void +rendermode_overlay_finish(void *data, RenderState *state) { + RenderModeOverlay *self = (RenderModeOverlay *)data; + + Py_DECREF(self->facemask_top); + Py_DECREF(self->white_color); + Py_DECREF(self->solid_blocks); + Py_DECREF(self->fluid_blocks); +} + +static int +rendermode_overlay_occluded(void *data, RenderState *state) { + int x = state->x, y = state->y, z = state->z; + + if ( (x != 0) && (y != 15) && (z != 127) && + !is_transparent(getArrayByte3D(state->blocks, x-1, y, z)) && + !is_transparent(getArrayByte3D(state->blocks, x, y, z+1)) && + !is_transparent(getArrayByte3D(state->blocks, x, y+1, z))) { + return 1; + } + + return 0; +} + +static void +rendermode_overlay_draw(void *data, RenderState *state, PyObject *src, PyObject *mask) { + RenderModeOverlay *self = (RenderModeOverlay *)data; + unsigned char r, g, b, a; + PyObject *top_block_py, *block_py; + + // exactly analogous to edge-line code for these special blocks + int increment=0; + if (state->block == 44) // half-step + increment=6; + else if (state->block == 78) // snow + increment=9; + + /* clear the draw space -- set alpha to 0 within mask */ + tint_with_mask(state->img, 255, 255, 255, 0, mask, state->imgx, state->imgy, 0, 0); + + /* skip rendering the overlay if we can't see it */ + if (state->z != 127) { + unsigned char top_block = getArrayByte3D(state->blocks, state->x, state->y, state->z+1); + if (!is_transparent(top_block)) { + return; + } + + /* check to be sure this block is solid/fluid */ + top_block_py = PyInt_FromLong(top_block); + if (PySequence_Contains(self->solid_blocks, top_block_py) || + PySequence_Contains(self->fluid_blocks, top_block_py)) { + + /* top block is fluid or solid, skip drawing */ + Py_DECREF(top_block_py); + return; + } + Py_DECREF(top_block_py); + } + + /* check to be sure this block is solid/fluid */ + block_py = PyInt_FromLong(state->block); + if (!PySequence_Contains(self->solid_blocks, block_py) && + !PySequence_Contains(self->fluid_blocks, block_py)) { + + /* not fluid or solid, skip drawing the overlay */ + Py_DECREF(block_py); + return; + } + Py_DECREF(block_py); + + /* get our color info */ + self->get_color(data, state, &r, &g, &b, &a); + + /* 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); + } +} + +RenderModeInterface rendermode_overlay = { + "overlay", "base rendermode for informational overlays", + NULL, + sizeof(RenderModeOverlay), + rendermode_overlay_start, + rendermode_overlay_finish, + rendermode_overlay_occluded, + rendermode_overlay_draw, +}; diff --git a/overviewer_core/src/rendermode-spawn.c b/overviewer_core/src/rendermode-spawn.c index b19f409..498ed17 100644 --- a/overviewer_core/src/rendermode-spawn.c +++ b/overviewer_core/src/rendermode-spawn.c @@ -18,21 +18,66 @@ #include "overviewer.h" #include +static void get_color(void *data, RenderState *state, + unsigned char *r, unsigned char *g, unsigned char *b, unsigned char *a) { + + RenderModeSpawn* self = (RenderModeSpawn *)data; + int x = state->x, y = state->y, z = state->z; + int z_light = z + 1; + unsigned char blocklight, skylight; + PyObject *block_py; + + /* set a nice, pretty red color */ + *r = 229; + *g = 36; + *b = 38; + + /* default to no overlay, until told otherwise */ + *a = 0; + + block_py = PyInt_FromLong(state->block); + if (PySequence_Contains(self->nospawn_blocks, block_py)) { + /* nothing can spawn on this */ + Py_DECREF(block_py); + return; + } + Py_DECREF(block_py); + + blocklight = getArrayByte3D(self->blocklight, x, y, MIN(127, z_light)); + + /* if we're at the top, force 15 (brightest!) skylight */ + if (z_light == 128) { + skylight = 15; + } else { + skylight = getArrayByte3D(self->skylight, x, y, z_light); + } + + if (MAX(blocklight, skylight) <= 7) { + /* hostile mobs spawn in daylight */ + *a = 240; + } else if (MAX(blocklight, skylight - 11) <= 7) { + /* hostile mobs spawn at night */ + *a = 150; + } +} + static int rendermode_spawn_start(void *data, RenderState *state) { RenderModeSpawn* self; /* first, chain up */ - int ret = rendermode_night.start(data, state); + int ret = rendermode_overlay.start(data, state); if (ret != 0) return ret; /* now do custom initializations */ self = (RenderModeSpawn *)data; - self->solid_blocks = PyObject_GetAttrString(state->chunk, "solid_blocks"); self->nospawn_blocks = PyObject_GetAttrString(state->chunk, "nospawn_blocks"); - self->fluid_blocks = PyObject_GetAttrString(state->chunk, "fluid_blocks"); - self->red_color = PyObject_GetAttrString(state->chunk, "red_color"); + self->blocklight = PyObject_GetAttrString(state->self, "blocklight"); + self->skylight = PyObject_GetAttrString(state->self, "skylight"); + + /* setup custom color */ + self->parent.get_color = get_color; return 0; } @@ -40,85 +85,31 @@ rendermode_spawn_start(void *data, RenderState *state) { static void rendermode_spawn_finish(void *data, RenderState *state) { /* first free all *our* stuff */ - RenderModeSpawn* self = (RenderModeSpawn *)data; + RenderModeSpawn* self = (RenderModeSpawn *)data; - Py_DECREF(self->solid_blocks); Py_DECREF(self->nospawn_blocks); - Py_DECREF(self->fluid_blocks); + Py_DECREF(self->blocklight); + Py_DECREF(self->skylight); /* now, chain up */ - rendermode_night.finish(data, state); + rendermode_overlay.finish(data, state); } static int rendermode_spawn_occluded(void *data, RenderState *state) { /* no special occlusion here */ - return rendermode_night.occluded(data, state); + return rendermode_overlay.occluded(data, state); } static void rendermode_spawn_draw(void *data, RenderState *state, PyObject *src, PyObject *mask) { - /* different versions of self (spawn, lighting) */ - RenderModeSpawn* self = (RenderModeSpawn *)data; - RenderModeLighting *lighting = (RenderModeLighting *)self; - - int x = state->x, y = state->y, z = state->z; - PyObject *old_black_color = NULL; - - /* figure out the appropriate darkness: - this block for transparents, the block above for non-transparent */ - float darkness = 0.0; - if (is_transparent(state->block)) { - darkness = get_lighting_coefficient((RenderModeLighting *)self, state, x, y, z, NULL); - } else { - darkness = get_lighting_coefficient((RenderModeLighting *)self, state, x, y, z+1, NULL); - } - - /* if it's dark enough... */ - if (darkness > 0.8) { - PyObject *block_py = PyInt_FromLong(state->block); - - /* make sure it's solid */ - if (PySequence_Contains(self->solid_blocks, block_py)) { - int spawnable = 1; - - /* not spawnable if its in the nospawn list */ - if (PySequence_Contains(self->nospawn_blocks, block_py)) - spawnable = 0; - - /* check the block above for solid or fluid */ - if (spawnable && z != 127) { - PyObject *top_block_py = PyInt_FromLong(getArrayByte3D(state->blocks, x, y, z+1)); - if (PySequence_Contains(self->solid_blocks, top_block_py) || - PySequence_Contains(self->fluid_blocks, top_block_py)) { - - spawnable = 0; - } - - Py_DECREF(top_block_py); - } - - /* if we passed all the checks, replace black_color with red_color */ - if (spawnable) { - old_black_color = lighting->black_color; - lighting->black_color = self->red_color; - } - } - - Py_DECREF(block_py); - } - /* draw normally */ - rendermode_night.draw(data, state, src, mask); - - /* reset black_color, if needed */ - if (old_black_color != NULL) { - lighting->black_color = old_black_color; - } + rendermode_overlay.draw(data, state, src, mask); } RenderModeInterface rendermode_spawn = { - "spawn", "draws red where monsters can spawn at night", + "spawn", "draws a red overlay where monsters can spawn at night", + &rendermode_overlay, sizeof(RenderModeSpawn), rendermode_spawn_start, rendermode_spawn_finish, diff --git a/overviewer_core/src/rendermodes.c b/overviewer_core/src/rendermodes.c index 0ef5550..43658e1 100644 --- a/overviewer_core/src/rendermodes.c +++ b/overviewer_core/src/rendermodes.c @@ -26,6 +26,7 @@ static RenderModeInterface *render_modes[] = { &rendermode_lighting, &rendermode_night, &rendermode_spawn, + &rendermode_cave, NULL }; @@ -97,5 +98,87 @@ PyObject *get_render_mode_info(PyObject *self, PyObject *args) { } Py_DECREF(info); - Py_RETURN_NONE; + return PyErr_Format(PyExc_ValueError, "invalid rendermode: \"%s\"", rendermode); +} + +/* bindings -- get parent's name */ +PyObject *get_render_mode_parent(PyObject *self, PyObject *args) { + const char *rendermode; + unsigned int i; + if (!PyArg_ParseTuple(args, "s", &rendermode)) + return NULL; + + for (i = 0; render_modes[i] != NULL; i++) { + if (strcmp(render_modes[i]->name, rendermode) == 0) { + if (render_modes[i]->parent) { + /* has parent */ + return PyString_FromString(render_modes[i]->parent->name); + } else { + /* no parent */ + Py_RETURN_NONE; + } + } + } + + return PyErr_Format(PyExc_ValueError, "invalid rendermode: \"%s\"", rendermode); +} + +/* bindings -- get list of inherited parents */ +PyObject *get_render_mode_inheritance(PyObject *self, PyObject *args) { + const char *rendermode; + PyObject *parents; + unsigned int i; + RenderModeInterface *iface = NULL; + if (!PyArg_ParseTuple(args, "s", &rendermode)) + return NULL; + + parents = PyList_New(0); + if (!parents) + return NULL; + + for (i = 0; render_modes[i] != NULL; i++) { + if (strcmp(render_modes[i]->name, rendermode) == 0) { + iface = render_modes[i]; + break; + } + } + + if (!iface) { + Py_DECREF(parents); + return PyErr_Format(PyExc_ValueError, "invalid rendermode: \"%s\"", rendermode); + } + + while (iface) { + PyObject *name = PyString_FromString(iface->name); + PyList_Append(parents, name); + Py_DECREF(name); + + iface = iface->parent; + } + + PyList_Reverse(parents); + return parents; +} + +/* bindings -- get list of (direct) children */ +PyObject *get_render_mode_children(PyObject *self, PyObject *args) { + const char *rendermode; + PyObject *children; + unsigned int i; + if (!PyArg_ParseTuple(args, "s", &rendermode)) + return NULL; + + children = PyList_New(0); + if (!children) + return NULL; + + for (i = 0; render_modes[i] != NULL; i++) { + if (render_modes[i]->parent && strcmp(render_modes[i]->parent->name, rendermode) == 0) { + PyObject *child_name = PyString_FromString(render_modes[i]->name); + PyList_Append(children, child_name); + Py_DECREF(child_name); + } + } + + return children; } diff --git a/overviewer_core/src/rendermodes.h b/overviewer_core/src/rendermodes.h index 3027a7f..fd0b479 100644 --- a/overviewer_core/src/rendermodes.h +++ b/overviewer_core/src/rendermodes.h @@ -38,12 +38,15 @@ #include /* rendermode interface */ -typedef struct { +typedef struct _RenderModeInterface RenderModeInterface; +struct _RenderModeInterface { /* the name of this mode */ const char* name; /* the short description of this render mode */ const char* description; + /* the rendermode this is derived from, or NULL */ + RenderModeInterface *parent; /* the size of the local storage for this rendermode */ unsigned int data_size; @@ -54,13 +57,16 @@ typedef struct { int (*occluded)(void *, RenderState *); /* last two arguments are img and mask, from texture lookup */ void (*draw)(void *, RenderState *, PyObject *, PyObject *); -} RenderModeInterface; +}; /* figures out the render mode to use from the given ChunkRenderer */ RenderModeInterface *get_render_mode(RenderState *state); /* python bindings */ PyObject *get_render_modes(PyObject *self, PyObject *args); PyObject *get_render_mode_info(PyObject *self, PyObject *args); +PyObject *get_render_mode_parent(PyObject *self, PyObject *args); +PyObject *get_render_mode_inheritance(PyObject *self, PyObject *args); +PyObject *get_render_mode_children(PyObject *self, PyObject *args); /* individual rendermode interface declarations follow */ @@ -79,6 +85,20 @@ typedef struct { } RenderModeNormal; extern RenderModeInterface rendermode_normal; +/* OVERLAY */ +typedef struct { + /* top facemask and white color image, for drawing overlays */ + PyObject *facemask_top, *white_color; + /* only show overlay on top of solid or fluid blocks */ + PyObject *solid_blocks, *fluid_blocks; + /* can be overridden in derived classes to control + overlay alpha and color + last four vars are r, g, b, a out */ + void (*get_color)(void *, RenderState *, + unsigned char *, unsigned char *, unsigned char *, unsigned char *); +} RenderModeOverlay; +extern RenderModeInterface rendermode_overlay; + /* LIGHTING */ typedef struct { /* inherits from normal render mode */ @@ -109,14 +129,31 @@ extern RenderModeInterface rendermode_night; /* SPAWN */ typedef struct { - /* inherits from night */ - RenderModeNight parent; + /* inherits from overlay */ + RenderModeOverlay parent; /* used to figure out which blocks are spawnable */ - PyObject *solid_blocks, *nospawn_blocks, *fluid_blocks; - /* replacement for black_color */ - PyObject *red_color; + PyObject *nospawn_blocks; + PyObject *skylight, *blocklight; } RenderModeSpawn; extern RenderModeInterface rendermode_spawn; +/* CAVE */ +typedef struct { + /* render blocks with lighting mode */ + RenderModeNormal parent; + + /* data used to know where the surface is */ + PyObject *skylight; + PyObject *left_skylight; + PyObject *right_skylight; + PyObject *up_left_skylight; + PyObject *up_right_skylight; + + /* colors used for tinting */ + PyObject *depth_colors; + +} RenderModeCave; +extern RenderModeInterface rendermode_cave; + #endif /* __RENDERMODES_H_INCLUDED__ */ diff --git a/overviewer_core/textures.py b/overviewer_core/textures.py index e3e714c..b5fb86a 100644 --- a/overviewer_core/textures.py +++ b/overviewer_core/textures.py @@ -121,14 +121,9 @@ def transform_image(img, blockID=None): """ - if blockID in (81,92): # cacti and cake - # Resize to 15x15, since the cactus and the cake textures are a little smaller than the other textures - img = img.resize((15, 15), Image.BILINEAR) - - else: - # Resize to 17x17, since the diagonal is approximately 24 pixels, a nice - # even number that can be split in half twice - img = img.resize((17, 17), Image.BILINEAR) + # Resize to 17x17, since the diagonal is approximately 24 pixels, a nice + # even number that can be split in half twice + img = img.resize((17, 17), Image.ANTIALIAS) # Build the Affine transformation matrix for this perspective transform = numpy.matrix(numpy.identity(3)) @@ -168,7 +163,7 @@ def transform_image_side(img, blockID=None): img = n # Size of the cube side before shear - img = img.resize((12,12)) + img = img.resize((12,12), Image.ANTIALIAS) # Apply shear transform = numpy.matrix(numpy.identity(3)) @@ -184,7 +179,7 @@ def transform_image_slope(img, blockID=None): in the -y direction (reflect for +x direction). Used for minetracks""" # Take the same size as trasform_image_side - img = img.resize((12,12)) + img = img.resize((12,12), Image.ANTIALIAS) # Apply shear transform = numpy.matrix(numpy.identity(3)) @@ -210,9 +205,8 @@ def _build_block(top, side, blockID=None): return img side = transform_image_side(side, blockID) - otherside = side.transpose(Image.FLIP_LEFT_RIGHT) - + # Darken the sides slightly. These methods also affect the alpha layer, # so save them first (we don't want to "darken" the alpha layer making # the block transparent) @@ -224,10 +218,7 @@ def _build_block(top, side, blockID=None): otherside.putalpha(othersidealpha) ## special case for non-block things - # TODO once torches are handled by generate_special_texture, remove - # them from this list - if blockID in (37,38,6,39,40,50,83,75,76): ## flowers, sapling, mushrooms, regular torch, reeds, - # redstone torch on, redstone torch off + if blockID in (37,38,6,39,40,83,30): ## flowers, sapling, mushrooms, reeds, web # # instead of pasting these blocks at the cube edges, place them in the middle: # and omit the top @@ -237,9 +228,9 @@ def _build_block(top, side, blockID=None): if blockID in (81,): # cacti! - composite.alpha_over(img, side, (2,6), side) - composite.alpha_over(img, otherside, (10,6), otherside) - composite.alpha_over(img, top, (0,2), top) + composite.alpha_over(img, side, (1,6), side) + composite.alpha_over(img, otherside, (11,6), otherside) + composite.alpha_over(img, top, (0,0), top) elif blockID in (44,): # half step # shift each texture down 6 pixels composite.alpha_over(img, side, (0,12), side) @@ -277,9 +268,37 @@ def _build_full_block(top, side1, side2, side3, side4, bottom=None, blockID=None side3 is in the -x (bottom left, north) side4 is in the +y (bottom right, west) - A non transparent block uses top, side 3 and side 4 + A non transparent block uses top, side 3 and side 4. + + If top is a tuple then first member is the top image and the second + member is an increment (integer) from 0 to 12. This increment will + used to crop the side images to look like a block and to paste all + the images increment pixels lower. Using increment = 6 will create + a half-block. + + NOTE: this method uses the top of the texture image (as done in + minecraft with beds) """ + + increment = 0 + if isinstance(top, tuple): + increment = top[1] + crop_height = int(increment * 16./12.) + top = top[0] + if side1 != None: + side1 = side1.copy() + ImageDraw.Draw(side1).rectangle((0, 16 - crop_height,16,16),outline=(0,0,0,0),fill=(0,0,0,0)) + if side2 != None: + side2 = side2.copy() + ImageDraw.Draw(side2).rectangle((0, 16 - crop_height,16,16),outline=(0,0,0,0),fill=(0,0,0,0)) + if side3 != None: + side3 = side3.copy() + ImageDraw.Draw(side3).rectangle((0, 16 - crop_height,16,16),outline=(0,0,0,0),fill=(0,0,0,0)) + if side4 != None: + side4 = side4.copy() + ImageDraw.Draw(side4).rectangle((0, 16 - crop_height,16,16),outline=(0,0,0,0),fill=(0,0,0,0)) + img = Image.new("RGBA", (24,24), (38,92,255,0)) # first back sides @@ -292,7 +311,7 @@ def _build_full_block(top, side1, side2, side3, side4, bottom=None, blockID=None side1 = ImageEnhance.Brightness(side1).enhance(0.9) side1.putalpha(sidealpha) - composite.alpha_over(img, side1, (0,0), side1) + composite.alpha_over(img, side1, (0,0 + increment), side1) if side2 != None : @@ -303,7 +322,7 @@ def _build_full_block(top, side1, side2, side3, side4, bottom=None, blockID=None side2 = ImageEnhance.Brightness(side2).enhance(0.8) side2.putalpha(sidealpha2) - composite.alpha_over(img, side2, (12,0), side2) + composite.alpha_over(img, side2, (12,0 + increment), side2) if bottom != None : bottom = transform_image(bottom, blockID) @@ -318,7 +337,7 @@ def _build_full_block(top, side1, side2, side3, side4, bottom=None, blockID=None side3 = ImageEnhance.Brightness(side3).enhance(0.9) side3.putalpha(sidealpha) - composite.alpha_over(img, side3, (0,6), side3) + composite.alpha_over(img, side3, (0,6 + increment), side3) if side4 != None : side4 = transform_image_side(side4, blockID) @@ -329,11 +348,11 @@ def _build_full_block(top, side1, side2, side3, side4, bottom=None, blockID=None side4 = ImageEnhance.Brightness(side4).enhance(0.8) side4.putalpha(sidealpha) - composite.alpha_over(img, side4, (12,6), side4) + composite.alpha_over(img, side4, (12,6 + increment), side4) if top != None : top = transform_image(top, blockID) - composite.alpha_over(img, top, (0,0), top) + composite.alpha_over(img, top, (0, increment), top) return img @@ -348,34 +367,34 @@ def _build_blockimages(): # texture array (terrain_images), which comes from terrain.png's cells, left to right top to # bottom. # 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 - topids = [ -1, 1, 0, 2, 16, 4, 15, 17,205,205,237,237, 18, 19, 32, 33, + topids = [ -1, 1, 0, 2, 16, 4, -1, 17,205,205,237,237, 18, 19, 32, 33, # 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 - 34, -1, 52, 48, 49,160,144, -1,176, 74, -1, -1, -1, -1, -1, -1, # Cloths are left out, sandstone (it has top, side, and bottom wich is ignored here), note block + 34, -1, 52, 48, 49,160,144, -1,176, 74, -1, -1, -1, -1, 11, -1, # 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 - -1, -1, -1, -1, -1, 13, 12, 29, 28, 23, 22, -1, -1, 7, 8, 35, # Gold/iron blocks? Doublestep? TNT from above? + -1, -1, -1, -1, -1, 13, 12, 29, 28, 23, 22, -1, -1, 7, 9, 4, # 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 - 36, 37, -1, -1, 65, -1, 25, -1, 98, 24, -1, -1, 86, -1, -1, -1, # Torch from above? leaving out fire. Redstone wire? Crops/furnaces handled elsewhere. sign post + 36, 37, -1, -1, 65, -1, -1, -1, 50, 24, -1, -1, 86, -1, -1, -1, # 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 - -1, -1, -1, -1, -1, -1, -1, -1, -1, 51, 51, -1, -1, -1, 66, 67, # door,ladder left out. Minecart rail orientation, redstone torches + -1, -1, -1, -1, -1, -1, -1, -1, -1, 51, 51, -1, -1, -1, 66, 67, # 80 81 82 83 84 85 86 87 88 89 90 91 - 66, 69, 72, 73, 74, -1,102,103,104,105,-1, 102 # clay? + 66, 69, 72, 73, 75, -1,102,103,104,105,-1, 102 # clay? ] # NOTE: For non-block textures, the sideid is ignored, but can't be -1 # And side textures of all block types # 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 - sideids = [ -1, 1, 3, 2, 16, 4, 15, 17,205,205,237,237, 18, 19, 32, 33, + sideids = [ -1, 1, 3, 2, 16, 4, -1, 17,205,205,237,237, 18, 19, 32, 33, # 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 - 34, -1, 52, 48, 49,160,144, -1,192, 74, -1, -1,- 1, -1, -1, -1, + 34, -1, 52, 48, 49,160,144, -1,192, 74, -1, -1,- 1, -1, 11, -1, # 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 -1, -1, -1, -1, -1, 13, 12, 29, 28, 23, 22, -1, -1, 7, 8, 35, # 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 - 36, 37, -1, -1, 65, -1, 25,101, 98, 24, -1, -1, 86, -1, -1, -1, + 36, 37, -1, -1, 65, -1, -1,101, 50, 24, -1, -1, 86, -1, -1, -1, # 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 -1, -1, -1, -1, -1, -1, -1, -1, -1, 51, 51, -1, -1, -1, 66, 67, # 80 81 82 83 84 85 86 87 88 89 90 91 - 66, 69, 72, 73, 74,-1 ,118,103,104,105, -1, 118 + 66, 70, 72, 73, 74,-1 ,118,103,104,105, -1, 118 ] # This maps block id to the texture that goes on the side of the block @@ -433,11 +452,41 @@ def generate_special_texture(blockID, data): # all need to behandled here (and in chunkpy) if blockID == 2: # grass - top = tintTexture(terrain_images[0],(115,175,71)) - img = _build_block(top, terrain_images[3], 2) + # data & 0x10 means SNOW sides + side_img = terrain_images[3] + if data & 0x10: + side_img = terrain_images[68] + img = _build_block(terrain_images[0], side_img, 2) + if not data & 0x10: + colored = tintTexture(biome_grass_texture, (115, 175, 71)) + composite.alpha_over(img, colored, (0, 0), colored) return (img.convert("RGB"), img.split()[3]) + if blockID == 6: # saplings + # The bottom two bits are used fo the sapling type, the top two + # bits are used as a grow-counter for the tree. + + if data & 0x3 == 0: # usual saplings + toptexture = terrain_images[15] + sidetexture = terrain_images[15] + + if data & 0x3 == 1: # spruce sapling + toptexture = terrain_images[63] + sidetexture = terrain_images[63] + + if data & 0x3 == 2: # birch sapling + toptexture = terrain_images[79] + sidetexture = terrain_images[79] + + if data & 0x3 == 3: # unused usual sapling + toptexture = terrain_images[15] + sidetexture = terrain_images[15] + + img = _build_block(toptexture, sidetexture, blockID) + return (img.convert("RGB"),img.split()[3]) + + if blockID == 9: # spring water, flowing water and waterfall water watertexture = _load_image("water.png") @@ -488,17 +537,51 @@ def generate_special_texture(blockID, data): t = tintTexture(terrain_images[52], (37, 118, 25)) img = _build_block(t, t, 18) return (img.convert("RGB"), img.split()[3]) - - if blockID == 23: # dispenser - top = transform_image(terrain_images[62]) - side1 = transform_image_side(terrain_images[46]) - side2 = transform_image_side(terrain_images[45]).transpose(Image.FLIP_LEFT_RIGHT) - img = Image.new("RGBA", (24,24), (38,92,255,0)) - - composite.alpha_over(img, side1, (0,6), side1) - composite.alpha_over(img, side2, (12,6), side2) - composite.alpha_over(img, top, (0,0), top) + + if blockID == 26: # bed + increment = 5 + left_face = None + right_face = None + if data & 0x8 == 0x8: # head of the bed + top = terrain_images[135] + if data & 0x00 == 0x00: # head pointing to West + top = top.copy().rotate(270) + left_face = terrain_images[151] + right_face = terrain_images[152] + if data & 0x01 == 0x01: # ... North + top = top.rotate(270) + left_face = terrain_images[152] + right_face = terrain_images[151] + if data & 0x02 == 0x02: # East + top = top.rotate(180) + left_face = terrain_images[151].transpose(Image.FLIP_LEFT_RIGHT) + right_face = None + if data & 0x03 == 0x03: # South + right_face = None + right_face = terrain_images[151].transpose(Image.FLIP_LEFT_RIGHT) + + else: # foot of the bed + top = terrain_images[134] + if data & 0x00 == 0x00: # head pointing to West + top = top.rotate(270) + left_face = terrain_images[150] + right_face = None + if data & 0x01 == 0x01: # ... North + top = top.rotate(270) + left_face = None + right_face = terrain_images[150] + if data & 0x02 == 0x02: # East + top = top.rotate(180) + left_face = terrain_images[150].transpose(Image.FLIP_LEFT_RIGHT) + right_face = terrain_images[149].transpose(Image.FLIP_LEFT_RIGHT) + if data & 0x03 == 0x03: # South + left_face = terrain_images[149] + right_face = terrain_images[150].transpose(Image.FLIP_LEFT_RIGHT) + + top = (top, increment) + img = _build_full_block(top, None, None, left_face, right_face) + return (img.convert("RGB"), img.split()[3]) @@ -731,6 +814,48 @@ def generate_special_texture(blockID, data): return (img.convert("RGB"), img.split()[3]) + if blockID == 54: # chests + # First to bits of the pseudo data store if it's a single chest + # or it's a double chest, first half or second half. + # The to last bits store the orientation. + + top = terrain_images[25] + side = terrain_images[26] + + if data & 12 == 0: # single chest + front = terrain_images[27] + back = terrain_images[26] + + elif data & 12 == 4: # double, first half + front = terrain_images[41] + back = terrain_images[57] + + elif data & 12 == 8: # double, second half + front = terrain_images[42] + back = terrain_images[58] + + else: # just in case + front = terrain_images[25] + side = terrain_images[25] + back = terrain_images[25] + + if data & 3 == 0: # facing west + img = _build_full_block(top, None, None, side, front) + + elif data & 3 == 1: # north + img = _build_full_block(top, None, None, front, side) + + elif data & 3 == 2: # east + img = _build_full_block(top, None, None, side, back) + + elif data & 3 == 3: # south + img = _build_full_block(top, None, None, back, side) + + else: + img = _build_full_block(top, None, None, back, side) + + return (img.convert("RGB"), img.split()[3]) + if blockID == 55: # redstone wire @@ -832,29 +957,28 @@ def generate_special_texture(blockID, data): return (img.convert("RGB"), img.split()[3]) - if blockID == 61: #furnace - top = transform_image(terrain_images[62]) - side1 = transform_image_side(terrain_images[45]) - side2 = transform_image_side(terrain_images[44]).transpose(Image.FLIP_LEFT_RIGHT) + if blockID in (61, 62, 23): #furnace and burning furnace + top = terrain_images[62] + side = terrain_images[45] - img = Image.new("RGBA", (24,24), (38,92,255,0)) + if blockID == 61: + front = terrain_images[44] - composite.alpha_over(img, side1, (0,6), side1) - composite.alpha_over(img, side2, (12,6), side2) - composite.alpha_over(img, top, (0,0), top) - return (img.convert("RGB"), img.split()[3]) + elif blockID == 62: + front = terrain_images[45+16] + elif blockID == 23: + front = terrain_images[46] - if blockID == 62: # lit furnace - top = transform_image(terrain_images[62]) - side1 = transform_image_side(terrain_images[45]) - side2 = transform_image_side(terrain_images[45+16]).transpose(Image.FLIP_LEFT_RIGHT) + if data == 3: # pointing west + img = _build_full_block(top, None, None, side, front) + + elif data == 4: # pointing north + img = _build_full_block(top, None, None, front, side) + + else: # in any other direction the front can't be seen + img = _build_full_block(top, None, None, side, side) - img = Image.new("RGBA", (24,24), (38,92,255,0)) - - composite.alpha_over(img, side1, (0,6), side1) - composite.alpha_over(img, side2, (12,6), side2) - composite.alpha_over(img, top, (0,0), top) return (img.convert("RGB"), img.split()[3]) @@ -938,11 +1062,27 @@ def generate_special_texture(blockID, data): return (img.convert("RGB"), img.split()[3]) - if blockID == 66: # minetrack: - - raw_straight = terrain_images[128] - raw_corner = terrain_images[112] + if blockID in (27, 28, 66): # minetrack: + if blockID == 27: # powered rail + if data & 0x8 == 0: # unpowered + raw_straight = terrain_images[163] + raw_corner = terrain_images[112] # they don't exist but make the code + # much simplier + elif data & 0x8 == 0x8: # powered + raw_straight = terrain_images[179] + raw_corner = terrain_images[112] # leave corners for code simplicity + # filter the 'powered' bit + data = data & 0x7 + + elif blockID == 28: # detector rail + raw_straight = terrain_images[195] + raw_corner = terrain_images[112] # leave corners for code simplicity + + elif blockID == 66: # normal rail + raw_straight = terrain_images[128] + raw_corner = terrain_images[112] + ## use transform_image to scale and shear if data == 0: track = transform_image(raw_straight, blockID) @@ -1034,9 +1174,9 @@ def generate_special_texture(blockID, data): # Compose the fence big stick fence_big = Image.new("RGBA", (24,24), (38,92,255,0)) - composite.alpha_over(fence_big,fence_side, (4,4),fence_side) - composite.alpha_over(fence_big,fence_other_side, (8,4),fence_other_side) - composite.alpha_over(fence_big,fence_top, (-1,1),fence_top) + composite.alpha_over(fence_big,fence_side, (5,4),fence_side) + composite.alpha_over(fence_big,fence_other_side, (7,4),fence_other_side) + composite.alpha_over(fence_big,fence_top, (0,1),fence_top) # Now render the small sticks. # Create needed images @@ -1098,16 +1238,36 @@ def generate_special_texture(blockID, data): if blockID in (86,91): # pumpkins, jack-o-lantern - top = transform_image(terrain_images[102]) + top = terrain_images[102] frontID = 119 if blockID == 86 else 120 - side1 = transform_image_side(terrain_images[frontID]) - side2 = transform_image_side(terrain_images[118]).transpose(Image.FLIP_LEFT_RIGHT) + front = terrain_images[frontID] + side = terrain_images[118] + if data == 0: # pointing west + img = _build_full_block(top, None, None, side, front) + + elif data == 1: # pointing north + img = _build_full_block(top, None, None, front, side) + + else: # in any other direction the front can't be seen + img = _build_full_block(top, None, None, side, side) + + return (img.convert("RGB"), img.split()[3]) + + + if blockID == 90: # portal + portaltexture = _load_image("portal.png") img = Image.new("RGBA", (24,24), (38,92,255,0)) - composite.alpha_over(img, side1, (0,6), side1) - composite.alpha_over(img, side2, (12,6), side2) - composite.alpha_over(img, top, (0,0), top) + side = transform_image_side(portaltexture) + otherside = side.transpose(Image.FLIP_TOP_BOTTOM) + + if data in (1,4): + composite.alpha_over(img, side, (5,4), side) + + if data in (2,8): + composite.alpha_over(img, otherside, (5,4), otherside) + return (img.convert("RGB"), img.split()[3]) @@ -1128,13 +1288,129 @@ def generate_special_texture(blockID, data): img = Image.new("RGBA", (24,24), (38,92,255,0)) - composite.alpha_over(img, side, (2,12), side) - composite.alpha_over(img, otherside, (10,12), otherside) - composite.alpha_over(img, top, (0,8), top) + composite.alpha_over(img, side, (1,12), side) + composite.alpha_over(img, otherside, (11,13), otherside) # workaround, fixes a hole + composite.alpha_over(img, otherside, (12,12), otherside) + composite.alpha_over(img, top, (0,6), top) + + return (img.convert("RGB"), img.split()[3]) + + + if blockID in (93, 94): # redstone repeaters, ON and OFF + # NOTE: this function uses the redstone torches generated above, + # this must run after the function of the torches. + + top = terrain_images[131] if blockID == 93 else terrain_images[147] + side = terrain_images[5] + increment = 9 - #~ composite.alpha_over(img, side, (2,6), side) - #~ composite.alpha_over(img, otherside, (10,6), otherside) - #~ composite.alpha_over(img, top, (0,2), top) + if (data & 0x3) == 0: # pointing east + pass + + if (data & 0x3) == 1: # pointing south + top = top.rotate(270) + + if (data & 0x3) == 2: # pointing west + top = top.rotate(180) + + if (data & 0x3) == 3: # pointing north + top = top.rotate(90) + + img = _build_full_block( (top, increment), None, None, side, side) + + # paste redstone torches everywhere! + t = specialblockmap[(75,5)] if blockID == 93 else specialblockmap[(76,5)] + torch = t[0].copy() # textures are stored as tuples (RGB,A) + torch.putalpha(t[1]) + + # the torch is too tall for the repeater, crop the bottom. + ImageDraw.Draw(torch).rectangle((0,16,24,24),outline=(0,0,0,0),fill=(0,0,0,0)) + + # touch up the 3d effect with big rectangles, just in case, for other texture packs + ImageDraw.Draw(torch).rectangle((0,24,10,15),outline=(0,0,0,0),fill=(0,0,0,0)) + ImageDraw.Draw(torch).rectangle((12,15,24,24),outline=(0,0,0,0),fill=(0,0,0,0)) + + # torch positions for every redstone torch orientation. + # + # This is a horrible list of torch orientations. I tried to + # obtain these orientations by rotating the positions for one + # orientation, but pixel rounding is horrible and messes the + # torches. + + if (data & 0x3) == 0: # pointing east + if (data & 0xC) == 0: # one tick delay + moving_torch = (1,1) + static_torch = (-3,-1) + + elif (data & 0xC) == 4: # two ticks delay + moving_torch = (2,2) + static_torch = (-3,-1) + + elif (data & 0xC) == 8: # three ticks delay + moving_torch = (3,2) + static_torch = (-3,-1) + + elif (data & 0xC) == 12: # four ticks delay + moving_torch = (4,3) + static_torch = (-3,-1) + + elif (data & 0x3) == 1: # pointing south + if (data & 0xC) == 0: # one tick delay + moving_torch = (1,1) + static_torch = (5,-1) + + elif (data & 0xC) == 4: # two ticks delay + moving_torch = (2,0) + static_torch = (5,-1) + + elif (data & 0xC) == 8: # three ticks delay + moving_torch = (3,0) + static_torch = (5,-1) + + elif (data & 0xC) == 12: # four ticks delay + moving_torch = (4,-1) + static_torch = (5,-1) + + elif (data & 0x3) == 2: # pointing west + if (data & 0xC) == 0: # one tick delay + moving_torch = (1,1) + static_torch = (5,3) + + elif (data & 0xC) == 4: # two ticks delay + moving_torch = (0,0) + static_torch = (5,3) + + elif (data & 0xC) == 8: # three ticks delay + moving_torch = (-1,0) + static_torch = (5,3) + + elif (data & 0xC) == 12: # four ticks delay + moving_torch = (-2,-1) + static_torch = (5,3) + + elif (data & 0x3) == 3: # pointing north + if (data & 0xC) == 0: # one tick delay + moving_torch = (1,1) + static_torch = (-3,3) + + elif (data & 0xC) == 4: # two ticks delay + moving_torch = (2,0) + static_torch = (-3,3) + + elif (data & 0xC) == 8: # three ticks delay + moving_torch = (3,0) + static_torch = (-3,3) + + elif (data & 0xC) == 12: # four ticks delay + moving_torch = (4,-1) + static_torch = (-3,3) + + # this paste order it's ok for east and south orientation + # but it's wrong for north and west orientations. But using the + # default texture pack the torches are small enough to no overlap. + composite.alpha_over(img, torch, static_torch, torch) + composite.alpha_over(img, torch, moving_torch, torch) + return (img.convert("RGB"), img.split()[3]) @@ -1147,7 +1423,7 @@ def tintTexture(im, c): return i # generate biome (still grayscale) leaf, grass textures -biome_grass_texture = _build_block(terrain_images[0], terrain_images[3], 2) +biome_grass_texture = _build_block(terrain_images[0], terrain_images[38], 2) biome_leaf_texture = _build_block(terrain_images[52], terrain_images[52], 18) @@ -1215,28 +1491,34 @@ def getBiomeData(worlddir, chunkX, chunkY): # (when adding new blocks here and in generate_special_textures, # please, if possible, keep the ascending order of blockid value) -special_blocks = set([2, 9, 17, 18, 23, 35, 43, 44, 50, 51, 53, 55, 58, 59, \ - 61, 62, 64, 65, 66, 67, 71, 75, 76, 85, 86, 91, 92]) +special_blocks = set([ 2, 6, 9, 17, 18, 26, 23, 27, 28, 35, 43, 44, 50, + 51, 53, 54, 55, 58, 59, 61, 62, 64, 65, 66, 67, 71, + 75, 76, 85, 86, 90, 91, 92, 93, 94]) # this is a map of special blockIDs to a list of all # possible values for ancillary data that it might have. special_map = {} -special_map[9] = range(32) # water: spring,flowing, waterfall, and others (unknown) ancildata values. +special_map[6] = range(16) # saplings: usual, spruce, birch and future ones (rendered as usual saplings) +special_map[9] = range(32) # water: spring,flowing, waterfall, and others (unknown) ancildata values, uses pseudo data special_map[17] = range(4) # wood: normal, birch and pine +special_map[26] = range(12) # bed, orientation special_map[23] = range(6) # dispensers, orientation +special_map[27] = range(14) # powered rail, orientation/slope and powered/unpowered +special_map[28] = range(6) # detector rail, orientation/slope special_map[35] = range(16) # wool, colored and white special_map[43] = range(4) # stone, sandstone, wooden and cobblestone double-slab special_map[44] = range(4) # stone, sandstone, wooden and cobblestone slab special_map[50] = (1,2,3,4,5) # torch, position in the block special_map[51] = range(16) # fire, position in the block (not implemented) special_map[53] = range(4) # wooden stairs, orientation -special_map[55] = range(128) # redstone wire, all the possible combinations +special_map[54] = range(12) # chests, orientation and type (single or double), uses pseudo data +special_map[55] = range(128) # redstone wire, all the possible combinations, uses pseudo data special_map[58] = (0,) # crafting table special_map[59] = range(8) # crops, grow from 0 to 7 -special_map[61] = range(6) # furnace, orientation (not implemented) -special_map[62] = range(6) # burning furnace, orientation (not implemented) +special_map[61] = range(6) # furnace, orientation +special_map[62] = range(6) # burning furnace, orientation special_map[64] = range(16) # wooden door, open/close and orientation special_map[65] = (2,3,4,5) # ladder, orientation special_map[66] = range(10) # minecrart tracks, orientation, slope @@ -1244,18 +1526,22 @@ special_map[67] = range(4) # cobblestone stairs, orientation special_map[71] = range(16) # iron door, open/close and orientation special_map[75] = (1,2,3,4,5) # off redstone torch, orientation special_map[76] = (1,2,3,4,5) # on redstone torch, orientation -special_map[85] = range(17) # fences, all the possible combination -special_map[86] = range(5) # pumpkin, orientation (not implemented) -special_map[91] = range(5) # jack-o-lantern, orientation (not implemented) +special_map[85] = range(17) # fences, all the possible combination, uses pseudo data +special_map[86] = range(5) # pumpkin, orientation +special_map[90] = (1,2,4,8) # portal, in 2 orientations, 4 cases, uses pseudo data +special_map[91] = range(5) # jack-o-lantern, orientation special_map[92] = range(6) # cake! +special_map[93] = range(16) # OFF redstone repeater, orientation and delay (delay not implemented) +special_map[94] = range(16) # ON redstone repeater, orientation and delay (delay not implemented) # grass and leaves are graysacle in terrain.png # we treat them as special so we can manually tint them # it is unknown how the specific tint (biomes) is calculated -special_map[2] = range(11) # grass, grass has not ancildata but is used - # in the mod WildGrass, and this small fix - # shows the map as expected, and is harmless - # for normal maps +# also, 0x10 means SNOW sides +special_map[2] = range(11) + [0x10,] # grass, grass has not ancildata but is + # used in the mod WildGrass, and this + # small fix shows the map as expected, + # and is harmless for normal maps special_map[18] = range(16) # leaves, birch, normal or pine leaves (not implemented) diff --git a/overviewer_core/util.py b/overviewer_core/util.py index 2e0bdbd..65c9f5c 100644 --- a/overviewer_core/util.py +++ b/overviewer_core/util.py @@ -41,7 +41,7 @@ def findGitVersion(): with open(os.path.join(this_dir,".git","HEAD")) as f: data = f.read().strip() if data.startswith("ref: "): - if not os.path.exists(os.path.join(this_dir,data[5:])): + if not os.path.exists(os.path.join(this_dir, ".git", data[5:])): return data with open(os.path.join(this_dir, ".git", data[5:])) as g: return g.read().strip() diff --git a/overviewer_core/world.py b/overviewer_core/world.py index b1b0ca7..7f7bab1 100644 --- a/overviewer_core/world.py +++ b/overviewer_core/world.py @@ -16,6 +16,7 @@ import functools import os import os.path +from glob import glob import multiprocessing import Queue import sys @@ -287,12 +288,10 @@ class World(object): p = f.split(".") yield (int(p[1]), int(p[2]), join(self.worlddir, 'region', f)) else: - for dirpath, dirnames, filenames in os.walk(os.path.join(self.worlddir, 'region')): - if not dirnames and filenames and "DIM-1" not in dirpath: - for f in filenames: - if f.startswith("r.") and f.endswith(".mcr"): - p = f.split(".") - yield (int(p[1]), int(p[2]), join(dirpath, f)) + for path in glob(os.path.join(self.worlddir, 'region') + "/r.*.*.mcr"): + dirpath, f = os.path.split(path) + p = f.split(".") + yield (int(p[1]), int(p[2]), join(dirpath, f)) def get_save_dir(): """Returns the path to the local saves directory diff --git a/sample.settings.py b/sample.settings.py index ee0d050..2872506 100644 --- a/sample.settings.py +++ b/sample.settings.py @@ -2,8 +2,9 @@ # Please see the README or https://github.com/brownan/Minecraft-Overviewer/wiki/DTT-Upgrade-Guide # for more details. -# To use this file, simply copy it to settings.py and make any necessary changes -# to suite your needs. +# This file is not meant to be used directly, but instead it is supposed to +# provide examples of interesting things you can do with the settings file. Most +# of the time, a simple 'setting_name = value' will work. # This file is a python script, so you can import and python module you wish or # use any built-in python function, though this is not normally necessary diff --git a/setup.py b/setup.py index 360e0d6..a961f66 100755 --- a/setup.py +++ b/setup.py @@ -21,6 +21,7 @@ except ImportError: setup_kwargs = {} setup_kwargs['ext_modules'] = [] setup_kwargs['cmdclass'] = {} +setup_kwargs['options'] = {} # # metadata @@ -41,7 +42,6 @@ if py2exe is not None: b = 3 else: b = 1 - setup_kwargs['options'] = {} setup_kwargs['options']['py2exe'] = {'bundle_files' : b, 'excludes': 'Tkinter'} # @@ -51,7 +51,7 @@ if py2exe is not None: setup_kwargs['packages'] = ['overviewer_core'] setup_kwargs['scripts'] = ['overviewer.py'] setup_kwargs['package_data'] = {'overviewer_core': - ['data/config.js', + ['data/overviewerConfig.js', 'data/textures/*', 'data/web_assets/*']} @@ -73,8 +73,11 @@ try: except: pil_include = [] -c_overviewer_files = ['main.c', 'composite.c', 'iterate.c', 'endian.c'] -c_overviewer_files += ['rendermodes.c', 'rendermode-normal.c', 'rendermode-lighting.c', 'rendermode-night.c', 'rendermode-spawn.c'] +# used to figure out what files to compile +render_modes = ['normal', 'overlay', 'lighting', 'night', 'spawn', 'cave'] + +c_overviewer_files = ['main.c', 'composite.c', 'iterate.c', 'endian.c', 'rendermodes.c'] +c_overviewer_files += map(lambda mode: 'rendermode-%s.c' % (mode,), render_modes) c_overviewer_files += ['Draw.c'] c_overviewer_includes = ['overviewer.h', 'rendermodes.h'] @@ -83,6 +86,7 @@ c_overviewer_includes = map(lambda s: 'overviewer_core/src/'+s, c_overviewer_inc setup_kwargs['ext_modules'].append(Extension('overviewer_core.c_overviewer', c_overviewer_files, include_dirs=['.', numpy_include] + pil_include, depends=c_overviewer_includes, extra_link_args=[])) + # tell build_ext to build the extension in-place # (NOT in build/) setup_kwargs['options']['build_ext'] = {'inplace' : 1}