0

merge and hash rewriting functions and permalinks

This commit is contained in:
Jeffrey Warren
2011-04-30 12:34:26 -04:00
43 changed files with 7321 additions and 2205 deletions

16
.gitignore vendored
View File

@@ -3,15 +3,21 @@ build
terrain.png
cachedir*
# vim swap files
.*.swp
# user-provided settings file
settings.py
# header files that may be copied over, if missing
ImPlatform.h
Imaging.h
# various forms of compiled _composite extensions
_composite.so
_composite.pyd
_composite_d.pyd
_composite.dylib
# various forms of compiled c_overviewer extensions
c_overviewer.so
c_overviewer.pyd
c_overviewer_d.pyd
c_overviewer.dylib
# Mac OS X noise
.DS_Store

53
CONTRIBUTORS.rst Normal file
View File

@@ -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 <brownan@gmail.com>
-------------------------
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 <fenixin@lavabit.com>
* Andrew Chin <achin@eminence32.net>
* Aaron Griffith <aargri@gmail.com>
* Alex Headley <aheadley@waysaboutstuff.com>
* Alex Jurkiewicz <alex@bluebottle.net.au>
* Xon <Xon@localhost>
------------------------
Short-term Contributions
------------------------
These contributors have made specific changes for a particular bug fix or
feature.
* arrai <array.of.intellect@gmail.com>
* Kyle Brantley <kyle@averageurl.com>
* cbarber <CraigBarber@taryx.com>
* Alex Cline <cline@vivisimo.com>
* Stephen Fluin <stephen@mistuph.com>
* Benjamin Herr <ben@0x539.de>
* Ryan Hitchman <hitchmanr@gmail.com>
* Jenny <jennytoo@gmail.com>
* Michael Jensen <emjay1988@gmail.com>
* Ryan McCue <ryanmccue@cubegames.net>
* Morlok8k <otis.spankmeyer@gmail.com>
* Gregory Short <gshort2@gmail.com>
* Sam Steele <sam@sigbox.c99.org>
* timwolla <timwolla@mail.develfusion.com>

View File

@@ -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
========
@@ -32,7 +37,7 @@ Features
* Renders efficiently in parallel, using as many simultaneous processes as you
want!
* Utilizes 2 levels of caching to speed up subsequent renderings of your world.
* Utilizes caching to speed up subsequent renderings of your world.
* Throw the output directory up on a web server to share your Minecraft world
with everyone!
@@ -46,6 +51,9 @@ This program requires:
* Numpy <http://scipy.org/Download>
* Either the Minecraft client installed, or a terrain.png file. See the
`Textures`_ section below.
* A C compiler.
If you download a binary package, then some or all of these may not be required.
I develop and test this on Linux, but need help testing it on Windows and Mac.
If something doesn't work, let me know.
@@ -53,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
@@ -86,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:
@@ -97,15 +111,14 @@ will use the biome data to tint grass and leaves automatically -- there is no
command line option to turn this feature on. If this folder does not exist,
then the Overviewer will use a static tinting for grass and leaves.
Compiling the C Extension (optional)
------------------------------------
The C Extension for Overviewer is completely optional. It provides a higher
quality image compositing function that looks better on maps with lighting
enabled, and a slight performance boost.
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.
If you downloaded Overviewer as a binary package, this extension may be already
compiled for you. Overviewer emits a warning if the extension is not found, but
will still work fine.
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::
@@ -117,24 +130,22 @@ 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 <http://svn.effbot.org/public/tags/pil-1.1.7/libImaging/>. 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
Running
-------
To generate a set of Google Map tiles, use the gmap.py script like this::
To generate a set of Google Map tiles, use the overviewer.py script like this::
python gmap.py [OPTIONS] <World # / Name / Path to World> <Output Directory>
python overviewer.py [OPTIONS] <World # / Name / Path to World> <Output Directory>
The output directory will be created if it doesn't exist. This will generate a
set of image tiles for your world in the directory you choose. When it's done,
you will find an index.html file in the same directory that you can use to view
it.
**Important note about Caches**
The Overviewer will put a cached image for every chunk *directly in your world
directory by default*. If you do not like this behavior, you can specify
another location with the --cachedir option. See below for details.
Options
-------
@@ -142,25 +153,6 @@ Options
-h, --help
Shows the list of options and exits
--cachedir=CACHEDIR
By default, the Overviewer will save in your world directory one image
file for every chunk in your world. If you do backups of your world,
you may not want these images in your world directory.
Use this option to specify an alternate location to put the rendered
chunk images. You must specify this same directory each rendering so
that it doesn't have to render every chunk from scratch every time.
Example::
python gmap.py --cachedir=<chunk cache dir> <world> <output dir>
--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
@@ -168,9 +160,56 @@ Options
Example to run 5 worker processes in parallel::
python gmap.py -p 5 <Path to World> <Output Directory>
python overviewer.py -p 5 <Path to World> <Output Directory>
-z ZOOM, --zoom=ZOOM
-d, --delete
This option changes the mode of execution. No tiles are rendered, and
instead, files are deleted.
*Note*: Currently only the overviewer.dat file is deleted when you run with
this option
--regionlist=regionlist
Use this option to specify manually a list of regions to consider for
updating. Without this option, every chunk in every region is checked for
update and if necessary, re-rendered. If this option points to a file
containing, 1 per line, the path to a region data file, then only those
in the list will be considered for update.
It's up to you to build such a list. On Linux or Mac, try using the "find"
command. You could, for example, output all region files that are older than
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!
--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.
--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.
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.
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.
@@ -189,71 +228,18 @@ Options
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 gmap.py -z 7 <Path to World> <Output Directory>
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, cache files are deleted.
Explanation: The Overviewer keeps two levels of cache: it saves each
chunk rendered as a png, and it keeps a hash file along side each tile
in your output directory. Using these cache files allows the Overviewer
to skip rendering of any tile image that has not changed.
By default, the chunk images are saved in your world directory. This
example will remove them::
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.
python gmap.py -d <World # / Path to World / Path to cache dir>
You can also delete the tile cache as well. This will force a full
re-render, useful if you've changed texture packs and want your world
to look uniform. Here's an example::
python gmap.py -d <# / path> <Tile Directory>
Be warned, this will cause the next rendering of your map to take
significantly longer, since it is having to re-generate the files you just
deleted.
--chunklist=CHUNKLIST
Use this option to specify manually a list of chunks to consider for
updating. Without this option, every chunk is checked for update and if
necessary, re-rendered. If this option points to a file containing, 1 per
line, the path to a chunk data file, then only those in the list will be
considered for update.
It's up to you to build such a list. On Linux or Mac, try using the "find"
command. You could, for example, output all chunk files that are older than
a certain date. Or perhaps you can incrementally update your map by passing
in a subset of chunks 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.
--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.
This function should accept one argument: a QuadtreeGen object.
Viewing the Results
-------------------
@@ -308,10 +294,5 @@ An incomplete list of things I want to do soon is:
* Improve efficiency
* Rendering non-cube blocks, such as torches, flowers, mine tracks, fences,
doors, and the like. Right now they are either not rendered at all, or
rendered as if they were a cube, so it looks funny.
* Some kind of graphical interface.
* A Windows exe for easier access for Windows users.

View File

@@ -1,224 +0,0 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
/*
* This file implements a custom alpha_over function for (some) PIL
* images. It's designed to be used through composite.py, which
* includes a proxy alpha_over function that falls back to the default
* PIL paste if this extension is not found.
*/
#include <Python.h>
#include <Imaging.h>
/* like (a * b + 127) / 255), but much faster on most platforms
from PIL's _imaging.c */
#define MULDIV255(a, b, tmp) \
(tmp = (a) * (b) + 128, ((((tmp) >> 8) + (tmp)) >> 8))
typedef struct
{
PyObject_HEAD
Imaging image;
} ImagingObject;
static Imaging imaging_python_to_c(PyObject* obj)
{
PyObject* im;
Imaging image;
/* first, get the 'im' attribute */
im = PyObject_GetAttrString(obj, "im");
if (!im)
return NULL;
/* make sure 'im' is the right type */
if (strcmp(im->ob_type->tp_name, "ImagingCore") != 0)
{
/* it's not -- raise an error and exit */
PyErr_SetString(PyExc_TypeError, "image attribute 'im' is not a core Imaging type");
return NULL;
}
image = ((ImagingObject*)im)->image;
Py_DECREF(im);
return image;
}
static PyObject* _composite_alpha_over(PyObject* self, PyObject* args)
{
/* raw input python variables */
PyObject* dest, * src, * pos, * mask;
/* libImaging handles */
Imaging imDest, imSrc, imMask;
/* cached blend properties */
int src_has_alpha, mask_offset, mask_stride;
/* destination position and size */
int dx, dy, xsize, ysize;
/* source position */
int sx, sy;
/* iteration variables */
unsigned int x, y, i;
/* temporary calculation variables */
int tmp1, tmp2, tmp3;
if (!PyArg_ParseTuple(args, "OOOO", &dest, &src, &pos, &mask))
return NULL;
imDest = imaging_python_to_c(dest);
imSrc = imaging_python_to_c(src);
imMask = imaging_python_to_c(mask);
if (!imDest || !imSrc || !imMask)
return NULL;
/* check the various image modes, make sure they make sense */
if (strcmp(imDest->mode, "RGBA") != 0)
{
PyErr_SetString(PyExc_ValueError, "given destination image does not have mode \"RGBA\"");
return NULL;
}
if (strcmp(imSrc->mode, "RGBA") != 0 && strcmp(imSrc->mode, "RGB") != 0)
{
PyErr_SetString(PyExc_ValueError, "given source image does not have mode \"RGBA\" or \"RGB\"");
return NULL;
}
if (strcmp(imMask->mode, "RGBA") != 0 && strcmp(imMask->mode, "L") != 0)
{
PyErr_SetString(PyExc_ValueError, "given mask image does not have mode \"RGBA\" or \"L\"");
return NULL;
}
/* make sure mask size matches src size */
if (imSrc->xsize != imMask->xsize || imSrc->ysize != imMask->ysize)
{
PyErr_SetString(PyExc_ValueError, "mask and source image sizes do not match");
return NULL;
}
/* set up flags for the src/mask type */
src_has_alpha = (imSrc->pixelsize == 4 ? 1 : 0);
/* how far into image the first alpha byte resides */
mask_offset = (imMask->pixelsize == 4 ? 3 : 0);
/* how many bytes to skip to get to the next alpha byte */
mask_stride = imMask->pixelsize;
/* destination position read */
if (!PyArg_ParseTuple(pos, "iiii", &dx, &dy, &xsize, &ysize))
{
PyErr_SetString(PyExc_TypeError, "given blend destination rect is not valid");
return NULL;
}
/* set up the source position, size and destination position */
/* handle negative dest pos */
if (dx < 0)
{
sx = -dx;
dx = 0;
} else {
sx = 0;
}
if (dy < 0)
{
sy = -dy;
dy = 0;
} else {
sy = 0;
}
/* set up source dimensions */
xsize -= sx;
ysize -= sy;
/* clip dimensions, if needed */
if (dx + xsize > imDest->xsize)
xsize = imDest->xsize - dx;
if (dy + ysize > imDest->ysize)
ysize = imDest->ysize - dy;
/* check that there remains any blending to be done */
if (xsize <= 0 || ysize <= 0)
{
/* nothing to do, return */
Py_INCREF(dest);
return dest;
}
for (y = 0; y < ysize; y++)
{
UINT8* out = (UINT8*) imDest->image[dy + y] + dx*4;
UINT8* outmask = (UINT8*) imDest->image[dy + y] + dx*4 + 3;
UINT8* in = (UINT8*) imSrc->image[sy + y] + sx*(imSrc->pixelsize);
UINT8* inmask = (UINT8*) imMask->image[sy + y] + sx*mask_stride + mask_offset;
for (x = 0; x < xsize; x++)
{
/* special cases */
if (*inmask == 255 || *outmask == 0)
{
*outmask = *inmask;
*out = *in;
out++, in++;
*out = *in;
out++, in++;
*out = *in;
out++, in++;
} else if (*inmask == 0) {
/* do nothing -- source is fully transparent */
out += 3;
in += 3;
} else {
/* general case */
int alpha = *inmask + MULDIV255(*outmask, 255 - *inmask, tmp1);
for (i = 0; i < 3; i++)
{
/* general case */
*out = MULDIV255(*in, *inmask, tmp1) + MULDIV255(MULDIV255(*out, *outmask, tmp2), 255 - *inmask, tmp3);
*out = (*out * 255) / alpha;
out++, in++;
}
*outmask = alpha;
}
out++;
if (src_has_alpha)
in++;
outmask += 4;
inmask += mask_stride;
}
}
Py_INCREF(dest);
return dest;
}
static PyMethodDef _CompositeMethods[] =
{
{"alpha_over", _composite_alpha_over, METH_VARARGS, "alpha over composite function"},
{NULL, NULL, 0, NULL}
};
PyMODINIT_FUNC init_composite(void)
{
(void) Py_InitModule("_composite", _CompositeMethods);
}

586
chunk.py
View File

@@ -16,7 +16,6 @@
import numpy
from PIL import Image, ImageDraw, ImageEnhance, ImageOps
import os.path
import hashlib
import logging
import time
import math
@@ -26,6 +25,7 @@ import nbt
import textures
import world
import composite
import c_overviewer
"""
This module has routines related to rendering one particular chunk into an
@@ -46,23 +46,29 @@ image
# alpha_over extension, BUT this extension may fall back to PIL's
# paste(), which DOES need the workaround.)
def get_lvldata(filename, x, y, retries=2):
def get_lvldata(world, filename, x, y, retries=2):
"""Takes a filename and chunkcoords and returns the Level struct, which contains all the
level info"""
# non existent region file doesn't mean corrupt chunk.
if filename == None:
raise NoSuchChunk
try:
d = nbt.load_from_region(filename, x, y)
d = world.load_from_region(filename, x, y)
except Exception, e:
if retries > 0:
# wait a little bit, and try again (up to `retries` times)
time.sleep(1)
return get_lvldata(filename, x, y, retries=retries-1)
#make sure we reload region info
world.reload_region(filename)
return get_lvldata(world, filename, x, y, retries=retries-1)
else:
logging.warning("Error opening chunk (%i, %i) in %s. It may be corrupt. %s", x, y, filename, e)
raise ChunkCorrupt(str(e))
if not d: raise NoSuchChunk(x,y)
return d[1]['Level']
return d
def get_blockarray(level):
"""Takes the level struct as returned from get_lvldata, and returns the
@@ -70,9 +76,9 @@ def get_blockarray(level):
return numpy.frombuffer(level['Blocks'], dtype=numpy.uint8).reshape((16,16,128))
def get_blockarray_fromfile(filename):
"""Same as get_blockarray except takes a filename and uses get_lvldata to
open it. This is a shortcut"""
level = get_lvldata(filename)
"""Same as get_blockarray except takes a filename. This is a shortcut"""
d = nbt.load_from_region(filename, x, y)
level = d[1]['Level']
return get_blockarray(level)
def get_skylight_array(level):
@@ -108,13 +114,14 @@ 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])
transparent_blocks = set([ 0, 6, 8, 9, 18, 20, 27, 28, 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])
# 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])
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])
@@ -123,70 +130,6 @@ fluid_blocks = set([8,9,10,11])
# (glass, half blocks)
nospawn_blocks = set([20,44])
def find_oldimage(chunkXY, cached, cave):
blockid = "%d.%d" % chunkXY
# Get the name of the existing image.
dir1 = world.base36encode(chunkXY[0]%64)
dir2 = world.base36encode(chunkXY[1]%64)
cachename = '/'.join((dir1, dir2))
oldimg = oldimg_path = None
key = ".".join((blockid, "cave" if cave else "nocave"))
if key in cached[cachename]:
oldimg_path = cached[cachename][key]
_, oldimg = os.path.split(oldimg_path)
#logging.debug("Found cached image {0}".format(oldimg))
return oldimg, oldimg_path
def check_cache(world, chunkXY, oldimg):
"""Returns True is oldimg is OK to use (i.e. not stale)"""
# TODO read to the region file and get the timestamp??
# TODO currently, just use the mtime on the region file
# TODO (which will cause a single chunk update to invalidate everything in the region
if not oldimg[1]: return False
chunkfile = os.path.join(world.worlddir, "region", "r.%d.%d.mcr" % (chunkXY[0]//32, chunkXY[1]//32))
with open(chunkfile, "rb") as f:
region = nbt.MCRFileReader(f)
mtime = region.get_chunk_timestamp(chunkXY[0], chunkXY[1])
#logging.debug("checking cache %s against %s %d", chunkfile, oldimg[1], mtime)
try:
if mtime <= os.path.getmtime(oldimg[1]):
return True
return False
except OSError:
return False
# chunkcoords should be the coordinates of a possible chunk. it may not exist
def render_and_save(chunkcoords, cachedir, worldobj, oldimg, cave=False, queue=None):
"""Used as the entry point for the multiprocessing workers (since processes
can't target bound methods) or to easily render and save one chunk
chunkcoords is a tuple: (chunkX, chunkY)
If the chunk doesn't exist, return None.
Else, returns the image file location"""
a = ChunkRenderer(chunkcoords, cachedir, worldobj, oldimg, queue)
try:
return a.render_and_save(cave)
except ChunkCorrupt:
# This should be non-fatal, but should print a warning
pass
except Exception, e:
import traceback
traceback.print_exc()
raise
except KeyboardInterrupt:
print
print "You pressed Ctrl-C. Exiting..."
# Raise an exception that is an instance of Exception. Unlike
# KeyboardInterrupt, this will re-raise in the parent, killing the
# entire program, instead of this process dying and the parent waiting
# forever for it to finish.
raise Exception()
class ChunkCorrupt(Exception):
pass
@@ -194,19 +137,17 @@ class NoSuchChunk(Exception):
pass
class ChunkRenderer(object):
def __init__(self, chunkcoords, cachedir, worldobj, oldimg, queue):
def __init__(self, chunkcoords, worldobj, rendermode, queue):
"""Make a new chunk renderer for the given chunk coordinates.
chunkcoors should be a tuple: (chunkX, chunkY)
cachedir is a directory to save the resulting chunk images to
"""
self.queue = queue
# derive based on worlddir and chunkcoords
self.regionfile = os.path.join(worldobj.worlddir, "region",
"r.%d.%d.mcr" % (chunkcoords[0] // 32, chunkcoords[1]//32))
if not os.path.exists(self.regionfile):
raise ValueError("Could not find regionfile: %s" % self.regionfile)
self.regionfile = worldobj.get_region_path(*chunkcoords)
#if not os.path.exists(self.regionfile):
# raise ValueError("Could not find regionfile: %s" % self.regionfile)
## TODO TODO all of this class
@@ -215,51 +156,21 @@ class ChunkRenderer(object):
#chunkcoords = filename_split[1:3]
#self.coords = map(world.base36decode, chunkcoords)
self.blockid = "%d.%d" % chunkcoords
#self.blockid = "%d.%d" % chunkcoords
# chunk coordinates (useful to converting local block coords to
# global block coords)
self.chunkX = chunkcoords[0]
self.chunkY = chunkcoords[1]
self.world = worldobj
# Cachedir here is the base directory of the caches. We need to go 2
# levels deeper according to the chunk file. Get the last 2 components
# of destdir and use that
##moredirs, dir2 = os.path.split(destdir)
##_, dir1 = os.path.split(moredirs)
self.cachedir = os.path.join(cachedir,
world.base36encode(self.chunkX%64),
world.base36encode(self.chunkY%64))
#logging.debug("cache location for this chunk: %s", self.cachedir)
self.oldimg, self.oldimg_path = oldimg
if self.world.useBiomeData:
# make sure we've at least *tried* to load the color arrays in this process...
textures.prepareBiomeData(self.world.worlddir)
if not textures.grasscolor or not textures.foliagecolor:
raise Exception("Can't find grasscolor.png or foliagecolor.png")
if not os.path.exists(self.cachedir):
try:
os.makedirs(self.cachedir)
except OSError, e:
import errno
if e.errno != errno.EEXIST:
raise
self.rendermode = rendermode
def _load_level(self):
"""Loads and returns the level structure"""
if not hasattr(self, "_level"):
try:
self._level = get_lvldata(self.regionfile, self.chunkX, self.chunkY)
self._level = get_lvldata(self.world,self.regionfile, self.chunkX, self.chunkY)
except NoSuchChunk, e:
logging.debug("Skipping non-existant chunk")
raise
@@ -291,7 +202,7 @@ class ChunkRenderer(object):
"""Loads and sets data from lower-left chunk"""
chunk_path = self.world.get_region_path(self.chunkX - 1, self.chunkY)
try:
chunk_data = get_lvldata(chunk_path, self.chunkX - 1, self.chunkY)
chunk_data = get_lvldata(self.world,chunk_path, self.chunkX - 1, self.chunkY)
self._left_skylight = get_skylight_array(chunk_data)
self._left_blocklight = get_blocklight_array(chunk_data)
self._left_blocks = get_blockarray(chunk_data)
@@ -325,7 +236,7 @@ class ChunkRenderer(object):
"""Loads and sets data from lower-right chunk"""
chunk_path = self.world.get_region_path(self.chunkX, self.chunkY + 1)
try:
chunk_data = get_lvldata(chunk_path, self.chunkX, self.chunkY + 1)
chunk_data = get_lvldata(self.world,chunk_path, self.chunkX, self.chunkY + 1)
self._right_skylight = get_skylight_array(chunk_data)
self._right_blocklight = get_blocklight_array(chunk_data)
self._right_blocks = get_blockarray(chunk_data)
@@ -359,7 +270,7 @@ class ChunkRenderer(object):
"""Loads and sets data from upper-right chunk"""
chunk_path = self.world.get_region_path(self.chunkX + 1, self.chunkY)
try:
chunk_data = get_lvldata(chunk_path, self.chunkX + 1, self.chunkY)
chunk_data = get_lvldata(self.world,chunk_path, self.chunkX + 1, self.chunkY)
self._up_right_skylight = get_skylight_array(chunk_data)
self._up_right_blocklight = get_blocklight_array(chunk_data)
self._up_right_blocks = get_blockarray(chunk_data)
@@ -374,12 +285,19 @@ 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)
try:
chunk_data = get_lvldata(chunk_path, self.chunkX, self.chunkY - 1)
chunk_data = get_lvldata(self.world,chunk_path, self.chunkX, self.chunkY - 1)
self._up_left_skylight = get_skylight_array(chunk_data)
self._up_left_blocklight = get_blocklight_array(chunk_data)
self._up_left_blocks = get_blockarray(chunk_data)
@@ -395,6 +313,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
@@ -434,16 +359,16 @@ class ChunkRenderer(object):
# and finally check for a block with same blockid. I we aren't in the border of a chunk,
# check for the block having the sme blockid.
if (up_right_blocks != None and up_right_blocks[0,y,z] == blockid) if x == 15 else blocks[x+1,y,z] == blockid:
if (up_right_blocks is not None and up_right_blocks[0,y,z] == blockid) if x == 15 else blocks[x+1,y,z] == blockid:
pseudo_data = pseudo_data | 0b1000
if (right_blocks != None and right_blocks[x,0,z] == blockid) if y == 15 else blocks[x,y + 1,z] == blockid:
if (right_blocks is not None and right_blocks[x,0,z] == blockid) if y == 15 else blocks[x,y + 1,z] == blockid:
pseudo_data = pseudo_data | 0b0100
if (left_blocks != None and left_blocks[15,y,z] == blockid) if x == 0 else blocks[x - 1,y,z] == blockid:
if (left_blocks is not None and left_blocks[15,y,z] == blockid) if x == 0 else blocks[x - 1,y,z] == blockid:
pseudo_data = pseudo_data | 0b0010
if (up_left_blocks != None and up_left_blocks[x,15,z] == blockid) if y == 0 else blocks[x,y - 1,z] == blockid:
if (up_left_blocks is not None and up_left_blocks[x,15,z] == blockid) if y == 0 else blocks[x,y - 1,z] == blockid:
pseudo_data = pseudo_data | 0b0001
# rotate the bits for other north orientations
@@ -456,181 +381,6 @@ class ChunkRenderer(object):
return pseudo_data
def _hash_blockarray(self):
"""Finds a hash of the block array"""
if hasattr(self, "_digest"):
return self._digest
h = hashlib.md5()
h.update(self.level['Blocks'])
# If the render algorithm changes, change this line to re-generate all
# the chunks automatically:
h.update("1")
digest = h.hexdigest()
# 6 digits ought to be plenty
self._digest = digest[:6]
return self._digest
def render_and_save(self, cave=False):
"""Render the chunk using chunk_render, and then save it to a file in
the same directory as the source image. If the file already exists and
is up to date, this method doesn't render anything.
"""
blockid = self.blockid
# Reasons for the code to get to this point:
# 1) An old image doesn't exist
# 2) An old image exists, but the chunk was more recently modified (the
# image was NOT checked if it was valid)
# 3) An old image exists, the chunk was not modified more recently, but
# the image was invalid and deleted (sort of the same as (1))
# What /should/ the image be named, go ahead and hash the block array
try:
dest_filename = "img.{0}.{1}.{2}.png".format(
blockid,
"cave" if cave else "nocave",
self._hash_blockarray(),
)
except NoSuchChunk, e:
return None
dest_path = os.path.join(self.cachedir, dest_filename)
#logging.debug("cache filename: %s", dest_path)
if self.oldimg:
if dest_filename == self.oldimg:
# There is an existing file, the chunk has a newer mtime, but the
# hashes match.
# Before we return it, update its mtime so the next round
# doesn't have to check the hash
# TODO confirm hash checking is correct (it should be)
os.utime(dest_path, None)
logging.debug("Using cached image, and updating utime")
return dest_path
else:
# Remove old image for this chunk. Anything already existing is
# either corrupt or out of date
os.unlink(self.oldimg_path)
logging.debug("doing a real real render")
# Render the chunk
img = self.chunk_render(cave=cave)
# Save it
try:
img.save(dest_path)
except:
os.unlink(dest_path)
raise
# Return its location
#raise Exception("early exit")
return dest_path
def calculate_darkness(self, skylight, blocklight):
"""Takes a raw blocklight and skylight, and returns a value
between 0.0 (fully lit) and 1.0 (fully black) that can be used as
an alpha value for a blend with a black source image. It mimics
Minecraft lighting calculations."""
if not self.world.night:
# Daytime
return 1.0 - pow(0.8, 15 - max(blocklight, skylight))
else:
# Nighttime
return 1.0 - pow(0.8, 15 - max(blocklight, skylight - 11))
def get_lighting_coefficient(self, x, y, z, norecurse=False):
"""Calculates the lighting coefficient for the given
coordinate, using default lighting and peeking into
neighboring chunks, if needed. A lighting coefficient of 1.0
means fully black.
Returns a tuple (coefficient, occluded), where occluded is
True if the given coordinate is filled with a solid block, and
therefore the returned coefficient is just the default."""
# placeholders for later data arrays, coordinates
blocks = None
skylight = None
blocklight = None
local_x = x
local_y = y
local_z = z
is_local_chunk = False
# find out what chunk we're in, and translate accordingly
if x >= 0 and y < 16:
blocks = self.blocks
skylight = self.skylight
blocklight = self.blocklight
is_local_chunk = True
elif x < 0:
local_x += 16
blocks = self.left_blocks
skylight = self.left_skylight
blocklight = self.left_blocklight
elif y >= 16:
local_y -= 16
blocks = self.right_blocks
skylight = self.right_skylight
blocklight = self.right_blocklight
# make sure we have a correctly-ranged coordinates and enough
# info about the chunk
if not (blocks != None and skylight != None and blocklight != None and
local_x >= 0 and local_x < 16 and local_y >= 0 and local_y < 16 and
local_z >= 0 and local_z < 128):
# we have no useful info, return default
return (self.calculate_darkness(15, 0), False)
blocktype = blocks[local_x, local_y, local_z]
# special handling for half-blocks
# (don't recurse more than once!)
if blocktype == 44 and not norecurse:
# average gathering variables
averagegather = 0.0
averagecount = 0
# how bright we need before we consider a side "lit"
threshold = self.calculate_darkness(0, 0)
# iterate through all the sides of the block
sides = [(x-1, y, z), (x+1, y, z), (x, y, z-1), (x, y, z+1), (x, y-1, z), (x, y+1, z)]
for side in sides:
val, occ = self.get_lighting_coefficient(*side, norecurse=True)
if (not occ) and (val < threshold):
averagegather += val
averagecount += 1
# if at least one side was lit, return the average
if averagecount > 0:
return (averagegather / averagecount, False)
# calculate the return...
occluded = not (blocktype in transparent_blocks)
# only calculate the non-default coefficient if we're not occluded
if (blocktype == 10) or (blocktype == 11):
# lava blocks should always be lit!
coefficient = 0.0
elif occluded:
coefficient = self.calculate_darkness(15, 0)
else:
coefficient = self.calculate_darkness(skylight[local_x, local_y, local_z], blocklight[local_x, local_y, local_z])
# only say we're occluded if the point is in the CURRENT
# chunk, so that we don't get obvious inter-chunk dependencies
# (we want this here so we still have the default coefficient
# for occluded blocks, even when we don't report them as
# occluded)
if not is_local_chunk:
occluded = False
return (coefficient, occluded)
def chunk_render(self, img=None, xoff=0, yoff=0, cave=False):
"""Renders a chunk with the given parameters, and returns the image.
If img is given, the chunk is rendered to that image object. Otherwise,
@@ -639,22 +389,7 @@ class ChunkRenderer(object):
For cave mode, all blocks that have any direct sunlight are not
rendered, and blocks are drawn with a color tint depending on their
depth."""
blocks = self.blocks
pseudo_ancildata_blocks = set([85])
left_blocks = self.left_blocks
right_blocks = self.right_blocks
if cave:
# Cave mode. Actually go through and 0 out all blocks that are not in a
# cave, so that it only renders caves.
# Places where the skylight is not 0 (there's some amount of skylight
# touching it) change it to something that won't get rendered, AND
# won't get counted as "transparent".
blocks = blocks.copy()
blocks[self.skylight != 0] = 21
blockData = get_blockdata_array(self.level)
blockData_expanded = numpy.empty((16,16,128), dtype=numpy.uint8)
# Even elements get the lower 4 bits
@@ -662,14 +397,6 @@ class ChunkRenderer(object):
# Odd elements get the upper 4 bits
blockData_expanded[:,:,1::2] = blockData >> 4
tileEntities = get_tileentity_data(self.level)
if self.world.useBiomeData:
biomeColorData = textures.getBiomeData(self.world.worlddir,
self.chunkX, self.chunkY)
# in the 32x32 block of biome data, what chunk is this?l
startX = self.chunkX % 32
startY = self.chunkY % 32
# Each block is 24x24
# The next block on the X axis adds 12px to x and subtracts 6px from y in the image
@@ -681,206 +408,9 @@ class ChunkRenderer(object):
if not img:
img = Image.new("RGBA", (384, 1728), (38,92,255,0))
for x in xrange(15,-1,-1):
for y in xrange(16):
imgx = xoff + x*12 + y*12
imgy = yoff - x*6 + y*6 + 128*12 + 16*12//2
imgy += 12
for z in xrange(128):
imgy -= 12
blockid = blocks[x,y,z]
# the following blocks don't have textures that can be pre-computed from the blockid
# alone. additional data is required.
# TODO torches, redstone torches, crops, ladders, stairs,
# levers, doors, buttons, and signs all need to be handled here (and in textures.py)
## minecart track, crops, ladder, doors, etc.
if blockid in textures.special_blocks:
# also handle furnaces here, since one side has a different texture than the other
ancilData = blockData_expanded[x,y,z]
try:
if blockid in pseudo_ancildata_blocks:
pseudo_ancilData = self.generate_pseudo_ancildata(x,y,z,blockid)
ancilData = pseudo_ancilData
t = textures.specialblockmap[(blockid, ancilData)]
except KeyError:
t = None
else:
t = textures.blockmap[blockid]
if not t:
continue
if self.world.useBiomeData:
# 16 : number of blocks in a chunk (in one direction)
# 32 : number of chunks in a region (and biome file) in one direction
# so 16 * 32 == 512 : number of blocks in biome file, in one direction
if blockid == 2: #grass
index = biomeColorData[ ((startY*16)+y) * 512 + (startX*16) + x]
c = textures.grasscolor[index]
# only tint the top texture
t = textures.prepareGrassTexture(c)
elif blockid == 18: # leaves
index = biomeColorData[ ((startY*16)+y) * 512 + (startX*16) + x]
c = textures.foliagecolor[index]
t = textures.prepareLeafTexture(c)
# Check if this block is occluded
if cave and (
x == 0 and y != 15 and z != 127
):
# If it's on the x face, only render if there's a
# transparent block in the y+1 direction OR the z-1
# direction
if (
blocks[x,y+1,z] not in transparent_blocks and
blocks[x,y,z+1] not in transparent_blocks
):
continue
elif cave and (
y == 15 and x != 0 and z != 127
):
# If it's on the facing y face, only render if there's
# a transparent block in the x-1 direction OR the z-1
# direction
if (
blocks[x-1,y,z] not in transparent_blocks and
blocks[x,y,z+1] not in transparent_blocks
):
continue
elif cave and (
y == 15 and x == 0 and z != 127
):
# If it's on the facing edge, only render if what's
# above it is transparent
if (
blocks[x,y,z+1] not in transparent_blocks
):
continue
elif (left_blocks == None and right_blocks == None):
# Normal block or not cave mode, check sides for
# transparentcy or render if it's a border chunk.
if (
x != 0 and y != 15 and z != 127 and
blocks[x-1,y,z] not in transparent_blocks and
blocks[x,y+1,z] not in transparent_blocks and
blocks[x,y,z+1] not in transparent_blocks
):
continue
elif (left_blocks != None and right_blocks == None):
if (
# If it has the left face covered check for
# transparent blocks in left face
y != 15 and z != 127 and
(left_blocks[15,y,z] if x == 0 else blocks[x - 1,y,z]) not in transparent_blocks and
blocks[x,y+1,z] not in transparent_blocks and
blocks[x,y,z+1] not in transparent_blocks
):
continue
elif (left_blocks == None and right_blocks != None):
if (
# If it has the right face covered check for
# transparent blocks in right face
x != 0 and z != 127 and
blocks[x-1,y,z] not in transparent_blocks and
(right_blocks[x,0,z] if y == 15 else blocks[x,y + 1,z]) not in transparent_blocks and
blocks[x,y,z+1] not in transparent_blocks
):
continue
elif (
# If it's a interior chunk check for transparent blocks
# in the adjacent chunks.
z != 127 and
(left_blocks[15,y,z] if x == 0 else blocks[x - 1,y,z]) not in transparent_blocks and
(right_blocks[x,0,z] if y == 15 else blocks[x,y + 1,z]) not in transparent_blocks and
blocks[x,y,z+1] not in transparent_blocks
# Don't render if all sides aren't transparent
):
continue
# Draw the actual block on the image. For cave images,
# tint the block with a color proportional to its depth
if cave:
# no lighting for cave -- depth is probably more useful
composite.alpha_over(img, Image.blend(t[0],depth_colors[z],0.3), (imgx, imgy), t[1])
else:
if not self.world.lighting:
# no lighting at all
composite.alpha_over(img, t[0], (imgx, imgy), t[1])
elif blockid in transparent_blocks:
# transparent means draw the whole
# block shaded with the current
# block's light
black_coeff, _ = self.get_lighting_coefficient(x, y, z)
if self.world.spawn and black_coeff > 0.8 and blockid in solid_blocks and not (
blockid in nospawn_blocks or (
z != 127 and (blocks[x,y,z+1] in solid_blocks or blocks[x,y,z+1] in fluid_blocks)
)
):
composite.alpha_over(img, Image.blend(t[0], red_color, black_coeff), (imgx, imgy), t[1])
else:
composite.alpha_over(img, Image.blend(t[0], black_color, black_coeff), (imgx, imgy), t[1])
else:
# draw each face lit appropriately,
# but first just draw the block
composite.alpha_over(img, t[0], (imgx, imgy), t[1])
# top face
black_coeff, face_occlude = self.get_lighting_coefficient(x, y, z + 1)
# Use red instead of black for spawnable blocks
if self.world.spawn and black_coeff > 0.8 and blockid in solid_blocks and not (
blockid in nospawn_blocks or (
z != 127 and (blocks[x,y,z+1] in solid_blocks or blocks[x,y,z+1] in fluid_blocks)
)
):
over_color = red_color
else:
over_color = black_color
if not face_occlude:
composite.alpha_over(img, over_color, (imgx, imgy), ImageEnhance.Brightness(facemasks[0]).enhance(black_coeff))
# left face
black_coeff, face_occlude = self.get_lighting_coefficient(x - 1, y, z)
if not face_occlude:
composite.alpha_over(img, over_color, (imgx, imgy), ImageEnhance.Brightness(facemasks[1]).enhance(black_coeff))
# right face
black_coeff, face_occlude = self.get_lighting_coefficient(x, y + 1, z)
if not face_occlude:
composite.alpha_over(img, over_color, (imgx, imgy), ImageEnhance.Brightness(facemasks[2]).enhance(black_coeff))
# Draw edge lines
if blockid in (44,): # step block
increment = 6
elif blockid in (78,): # snow
increment = 9
else:
increment = 0
if blockid not in transparent_blocks or blockid in (78,): #special case snow so the outline is still drawn
draw = ImageDraw.Draw(img)
if x != 15 and blocks[x+1,y,z] == 0:
draw.line(((imgx+12,imgy+increment), (imgx+22,imgy+5+increment)), fill=(0,0,0), width=1)
if y != 0 and blocks[x,y-1,z] == 0:
draw.line(((imgx,imgy+6+increment), (imgx+12,imgy+increment)), fill=(0,0,0), width=1)
c_overviewer.render_loop(self, img, xoff, yoff, blockData_expanded)
tileEntities = get_tileentity_data(self.level)
for entity in tileEntities:
if entity['id'] == 'Sign':
msg=' \n'.join([entity['Text1'], entity['Text2'], entity['Text3'], entity['Text4']])
@@ -894,13 +424,15 @@ class ChunkRenderer(object):
msg=msg,
chunk= (self.chunkX, self.chunkY),
)
self.queue.put(["newpoi", newPOI])
if self.queue:
self.queue.put(["newpoi", newPOI])
# check to see if there are any signs in the persistentData list that are from this chunk.
# if so, remove them from the persistentData list (since they're have been added to the world.POI
# list above.
self.queue.put(['removePOI', (self.chunkX, self.chunkY)])
if self.queue:
self.queue.put(['removePOI', (self.chunkX, self.chunkY)])
return img
@@ -932,7 +464,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():
@@ -941,8 +473,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:

View File

@@ -23,12 +23,7 @@ Overviewer. It defaults to the PIL paste function when the custom
alpha-over extension cannot be found.
"""
extension_alpha_over = None
try:
from _composite import alpha_over as _extension_alpha_over
extension_alpha_over = _extension_alpha_over
except ImportError:
pass
from c_overviewer import alpha_over as extension_alpha_over
def alpha_over(dest, src, pos_or_rect=(0, 0), mask=None):
"""Composite src over dest, using mask as the alpha channel (if
@@ -36,16 +31,8 @@ def alpha_over(dest, src, pos_or_rect=(0, 0), mask=None):
either be a position or a rectangle, specifying where on dest to
put src. Falls back to dest.paste() if the alpha_over extension
can't be found."""
if mask == None:
if mask is None:
mask = src
global extension_alpha_over
if extension_alpha_over != None:
# extension ALWAYS expects rects, so convert if needed
if len(pos_or_rect) == 2:
pos_or_rect = (pos_or_rect[0], pos_or_rect[1], src.size[0], src.size[1])
extension_alpha_over(dest, src, pos_or_rect, mask)
else:
# fallback
dest.paste(src, pos_or_rect, mask)
return extension_alpha_over(dest, src, pos_or_rect, mask)

View File

@@ -1,56 +0,0 @@
var config = {
fileExt: '{imgformat}',
tileSize: 384,
defaultZoom: 1,
maxZoom: {maxzoom},
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/'}
];
// Please leave the following variables here:
var markerCollection = {}; // holds groups of markers
var map;
var markersInit = false;
var regionsInit = false;

173
configParser.py Normal file
View File

@@ -0,0 +1,173 @@
from optparse import OptionParser
import sys
import os.path
import logging
class OptionsResults(object):
def get(self, *args):
return self.__dict__.get(*args)
class ConfigOptionParser(object):
def __init__(self, **kwargs):
self.cmdParser = OptionParser(usage=kwargs.get("usage",""))
self.configFile = kwargs.get("config","settings.py")
self.configVars = []
# these are arguments not understood by OptionParser, so they must be removed
# in add_option before being passed to the OptionParser
# note that default is a valid OptionParser argument, but we remove it
# because we want to do our default value handling
self.customArgs = ["required", "commandLineOnly", "default", "listify", "listdelim", "choices"]
self.requiredArgs = []
def display_config(self):
logging.info("Using the following settings:")
for x in self.configVars:
n = x['dest']
print "%s: %r" % (n, self.configResults.__dict__[n])
def add_option(self, *args, **kwargs):
if kwargs.get("configFileOnly", False) and kwargs.get("commandLineOnly", False):
raise Exception(args, "configFileOnly and commandLineOnly are mututally exclusive")
self.configVars.append(kwargs.copy())
if not kwargs.get("configFileOnly", False):
for arg in self.customArgs:
if arg in kwargs.keys(): del kwargs[arg]
if kwargs.get("type", None):
kwargs['type'] = 'string' # we'll do our own converting later
self.cmdParser.add_option(*args, **kwargs)
def print_help(self):
self.cmdParser.print_help()
def parse_args(self):
# first, load the results from the command line:
options, args = self.cmdParser.parse_args()
# second, use these values to seed the locals dict
l = dict()
g = dict()
for a in self.configVars:
n = a['dest']
if a.get('configFileOnly', False): continue
if a.get('commandLineOnly', False): continue
v = getattr(options, n)
if v != None:
#print "seeding %s with %s" % (n, v)
l[n] = v
else:
# if this has a default, use that to seed the globals dict
if a.get("default", None): g[n] = a['default']
g['args'] = args
try:
if os.path.exists(self.configFile):
execfile(self.configFile, g, l)
except NameError, ex:
import traceback
traceback.print_exc()
logging.error("Error parsing %s. Please check the trackback above" % self.configFile)
sys.exit(1)
except SyntaxError, ex:
import traceback
traceback.print_exc()
tb = sys.exc_info()[2]
#print tb.tb_frame.f_code.co_filename
logging.error("Error parsing %s. Please check the trackback above" % self.configFile)
sys.exit(1)
#print l.keys()
configResults = OptionsResults()
# third, load the results from the config file:
for a in self.configVars:
n = a['dest']
if a.get('commandLineOnly', False):
if n in l.keys():
logging.error("Error: %s can only be specified on the command line. It is not valid in the config file" % n)
sys.exit(1)
configResults.__dict__[n] = l.get(n)
# third, merge options into configReslts (with options overwriting anything in configResults)
for a in self.configVars:
n = a['dest']
if a.get('configFileOnly', False): continue
if getattr(options, n) != None:
configResults.__dict__[n] = getattr(options, n)
# forth, set defaults for any empty values
for a in self.configVars:
n = a['dest']
if (n not in configResults.__dict__.keys() or configResults.__dict__[n] == None) and 'default' in a.keys():
configResults.__dict__[n] = a['default']
# fifth, check required args:
for a in self.configVars:
n = a['dest']
if configResults.__dict__[n] == None and a.get('required',False):
logging.error("%s is required" % n)
sys.exit(1)
# sixth, check types
for a in self.configVars:
n = a['dest']
if 'listify' in a.keys():
# this thing may be a list!
if configResults.__dict__[n] != None and type(configResults.__dict__[n]) == str:
configResults.__dict__[n] = configResults.__dict__[n].split(a.get("listdelim",","))
elif type(configResults.__dict__[n]) != list:
configResults.__dict__[n] = [configResults.__dict__[n]]
if 'type' in a.keys() and configResults.__dict__[n] != None:
try:
configResults.__dict__[n] = self.checkType(configResults.__dict__[n], a)
except ValueError, ex:
logging.error("There was a problem converting the value '%s' to type %s for config parameter '%s'" % (configResults.__dict__[n], a['type'], n))
import traceback
#traceback.print_exc()
sys.exit(1)
self.configResults = configResults
return configResults, args
def checkType(self, value, a):
if type(value) == list:
return map(lambda x: self.checkType(x, a), value)
# switch on type. there are only 7 types that can be used with optparse
if a['type'] == "int":
return int(value)
elif a['type'] == "string":
return str(value)
elif a['type'] == "long":
return long(value)
elif a['type'] == "choice":
if value not in a['choices']:
logging.error("The value '%s' is not valid for config parameter '%s'" % (value, a['dest']))
sys.exit(1)
return value
elif a['type'] == "float":
return long(value)
elif a['type'] == "complex":
return complex(value)
elif a['type'] == "function":
if not callable(value):
raise ValueError("Not callable")
else:
logging.error("Unknown type!")
sys.exit(1)

51
contrib/benchmark.py Normal file
View File

@@ -0,0 +1,51 @@
import chunk
import world
import tempfile
import glob
import time
import cProfile
import os
import sys
import shutil
# Simple Benchmarking script. Usage and example:
# $ python contrib/benchmark.py World4/
# Rendering 50 chunks...
# Took 20.290062 seconds or 0.405801 seconds per chunk, or 2.464261 chunks per second
# create a new, empty, cache dir
cachedir = tempfile.mkdtemp(prefix="benchmark_cache", dir=".")
if os.path.exists("benchmark.prof"): os.unlink("benchmark.prof")
w = world.WorldRenderer("World4", cachedir)
numchunks = 50
chunklist = w._find_chunkfiles()[:numchunks]
print "Rendering %d chunks..." % (numchunks)
def go():
for f in chunklist:
chunk.render_and_save(f[2], w.cachedir, w, (None,None), None)
start = time.time()
if "-profile" in sys.argv:
cProfile.run("go()", 'benchmark.prof')
else:
go()
stop = time.time()
delta = stop - start
print "Took %f seconds or %f seconds per chunk, or %f chunks per second" % (delta, delta/numchunks, numchunks/delta)
if "-profile" in sys.argv:
print "Profile is below:\n----\n"
import pstats
p = pstats.Stats('benchmark.prof')
p.strip_dirs().sort_stats("cumulative").print_stats(20)
shutil.rmtree(cachedir)

94
contrib/clearOldCache.py Normal file
View File

@@ -0,0 +1,94 @@
#!/usr/bin/python
usage = "python contrib/%prog [OPTIONS] <World # / Name / Path to World>"
description = """
This script will delete files from the old chunk-based cache, a lot
like the old `overviewer.py -d World/` command. You should only use this if
you're updating from an older version of Overviewer, and you want to
clean up your world folder.
"""
from optparse import OptionParser
import sys
import re
import os.path
# sys.path wrangling, so we can access Overviewer code
overviewer_dir = os.path.split(os.path.split(os.path.abspath(__file__))[0])[0]
sys.path.insert(0, overviewer_dir)
import world
from overviewer import list_worlds
def main():
parser = OptionParser(usage=usage, description=description)
parser.add_option("-d", "--dry-run", dest="dry", action="store_true",
help="Don't actually delete anything. Best used with -v.")
parser.add_option("-k", "--keep-dirs", dest="keep", action="store_true",
help="Keep the world directories intact, even if they are empty.")
parser.add_option("-v", "--verbose", dest="verbose", action="store_true",
help="Log each and every file that is deleted.")
opt, args = parser.parse_args()
if not len(args) == 1:
parser.print_help()
sys.exit(1)
worlddir = args[0]
if not os.path.exists(worlddir):
# world given is either world number, or name
worlds = world.get_worlds()
# if there are no worlds found at all, exit now
if not worlds:
parser.print_help()
print "\nInvalid world path"
sys.exit(1)
try:
worldnum = int(worlddir)
worlddir = worlds[worldnum]['path']
except ValueError:
# it wasn't a number or path, try using it as a name
try:
worlddir = worlds[worlddir]['path']
except KeyError:
# it's not a number, name, or path
parser.print_help()
print "Invalid world name or path"
sys.exit(1)
except KeyError:
# it was an invalid number
parser.print_help()
print "Invalid world number"
sys.exit(1)
files_deleted = 0
dirs_deleted = 0
imgre = re.compile(r'img\.[^.]+\.[^.]+\.nocave\.\w+\.png$')
for dirpath, dirnames, filenames in os.walk(worlddir, topdown=False):
for f in filenames:
if imgre.match(f):
filepath = os.path.join(dirpath, f)
if opt.verbose:
print "Deleting %s" % (filepath,)
if not opt.dry:
os.unlink(filepath)
files_deleted += 1
if not opt.keep:
if len(os.listdir(dirpath)) == 0:
if opt.verbose:
print "Deleting %s" % (dirpath,)
if not opt.dry:
os.rmdir(dirpath)
dirs_deleted += 1
print "%i files and %i directories deleted." % (files_deleted, dirs_deleted)
if __name__ == "__main__":
main()

View File

@@ -11,7 +11,7 @@ To run, simply give a path to your world directory, for example:
Once that is done, simply re-run the overviewer to generate markers.js:
python gmap.py ../world.test/ output_dir/
python overviewer.py ../world.test/ output_dir/
Note: if your cachedir is not the same as your world-dir, you'll need to manually
move overviewer.dat into the correct location.
@@ -33,30 +33,34 @@ if os.path.exists(worlddir):
else:
sys.exit("Bad WorldDir")
matcher = re.compile(r"^c\..*\.dat$")
matcher = re.compile(r"^r\..*\.mcr$")
POI = []
for dirpath, dirnames, filenames in os.walk(worlddir):
for f in filenames:
if matcher.match(f):
print f
full = os.path.join(dirpath, f)
#print "inspecting %s" % full
data = nbt.load(full)[1]['Level']['TileEntities']
for entity in data:
if entity['id'] == 'Sign':
msg=' \n'.join([entity['Text1'], entity['Text2'], entity['Text3'], entity['Text4']])
#print "checking -->%s<--" % msg.strip()
if msg.strip():
newPOI = dict(type="sign",
x= entity['x'],
y= entity['y'],
z= entity['z'],
msg=msg,
chunk= (entity['x']/16, entity['z']/16),
)
POI.append(newPOI)
print "Found sign at (%d, %d, %d): %r" % (newPOI['x'], newPOI['y'], newPOI['z'], newPOI['msg'])
r = nbt.load_region(full)
chunks = r.get_chunks()
for x,y in chunks:
chunk = r.load_chunk(x,y).read_all()
data = chunk[1]['Level']['TileEntities']
for entity in data:
if entity['id'] == 'Sign':
msg=' \n'.join([entity['Text1'], entity['Text2'], entity['Text3'], entity['Text4']])
#print "checking -->%s<--" % msg.strip()
if msg.strip():
newPOI = dict(type="sign",
x= entity['x'],
y= entity['y'],
z= entity['z'],
msg=msg,
chunk= (entity['x']/16, entity['z']/16),
)
POI.append(newPOI)
print "Found sign at (%d, %d, %d): %r" % (newPOI['x'], newPOI['y'], newPOI['z'], newPOI['msg'])

View File

@@ -3,12 +3,12 @@
'''
This is used to force the regeneration of any chunks that contain a certain
blockID. The output is a chunklist file that is suitable to use with the
--chunklist option to gmap.py.
--chunklist option to overviewer.py.
Example:
python contrib/rerenderBlocks.py --ids=46,79,91 --world=world/> chunklist.txt
python gmap.py --chunklist=chunklist.txt world/ output_dir/
python contrib/rerenderBlocks.py --ids=46,79,91 --world=world/> regionlist.txt
python overviewer.py --regionlist=regionlist.txt world/ output_dir/
This will rerender any chunks that contain either TNT (46), Ice (79), or
a Jack-O-Lantern (91)
@@ -42,15 +42,20 @@ ids = map(lambda x: int(x),options.ids.split(","))
sys.stderr.write("Searching for these blocks: %r...\n" % ids)
matcher = re.compile(r"^c\..*\.dat$")
matcher = re.compile(r"^r\..*\.mcr$")
for dirpath, dirnames, filenames in os.walk(options.world):
for f in filenames:
if matcher.match(f):
full = os.path.join(dirpath, f)
blocks = get_blockarray_fromfile(full)
for i in ids:
if i in blocks:
print full
break
r = nbt.load_region(full)
chunks = r.get_chunks()
for x,y in chunks:
chunk = r.load_chunk(x,y).read_all()
blocks = get_blockarray(chunk[1]['Level'])
for i in ids:
if i in blocks:
print full
break

156
contrib/testRender.py Normal file
View File

@@ -0,0 +1,156 @@
#!/usr/bin/python
import os, shutil, tempfile, time, sys, math, re
from subprocess import Popen, PIPE, STDOUT, CalledProcessError
from optparse import OptionParser
overviewer_scripts = ['./overviewer.py', './gmap.py']
def check_call(*args, **kwargs):
quiet = False
if "quiet" in kwargs.keys():
quiet = kwargs["quiet"]
del kwargs["quiet"]
if quiet:
kwargs['stdout'] = PIPE
kwargs['stderr'] = STDOUT
p = Popen(*args, **kwargs)
output = ""
if quiet:
while p.poll() == None:
output += p.communicate()[0]
returncode = p.wait()
if returncode:
if quiet:
print output
raise CalledProcessError(returncode, args)
return returncode
def check_output(*args, **kwargs):
kwargs['stdout'] = PIPE
# will hang for HUGE output... you were warned
p = Popen(*args, **kwargs)
returncode = p.wait()
if returncode:
raise CalledProcessError(returncode, args)
return p.communicate()[0]
def clean_render(overviewerargs, quiet):
tempdir = tempfile.mkdtemp('mc-overviewer-test')
overviewer_script = None
for script in overviewer_scripts:
if os.path.exists(script):
overviewer_script = script
break
if overviewer_script is None:
sys.stderr.write("could not find main overviewer script\n")
sys.exit(1)
try:
# check_call raises CalledProcessError when overviewer.py exits badly
check_call(['python', 'setup.py', 'clean', 'build'], quiet=quiet)
check_call([overviewer_script, '-d'] + overviewerargs, quiet=quiet)
starttime = time.time()
check_call([overviewer_script,] + overviewerargs + [tempdir,], quiet=quiet)
endtime = time.time()
return endtime - starttime
finally:
shutil.rmtree(tempdir, True)
def get_stats(timelist):
stats = {}
stats['count'] = len(timelist)
stats['minimum'] = min(timelist)
stats['maximum'] = max(timelist)
stats['average'] = sum(timelist) / float(len(timelist))
meandiff = map(lambda x: (x - stats['average'])**2, timelist)
stats['standard deviation'] = math.sqrt(sum(meandiff) / float(len(meandiff)))
return stats
commitre = re.compile('^commit ([a-z0-9]{40})$', re.MULTILINE)
branchre = re.compile('^\\* (.+)$', re.MULTILINE)
def get_current_commit():
gittext = check_output(['git', 'branch'])
match = branchre.search(gittext)
if match and not ("no branch" in match.group(1)):
return match.group(1)
gittext = check_output(['git', 'show', 'HEAD'])
match = commitre.match(gittext)
if match == None:
return None
return match.group(1)
def get_commits(gitrange):
gittext = check_output(['git', 'log', '--raw', '--reverse', gitrange])
for match in commitre.finditer(gittext):
yield match.group(1)
def set_commit(commit):
check_call(['git', 'checkout', commit], quiet=True)
parser = OptionParser(usage="usage: %prog [options] -- [overviewer options/world]")
parser.add_option("-n", "--number", metavar="N",
action="store", type="int", dest="number", default=3,
help="number of renders per commit [default: 3]")
parser.add_option("-c", "--commits", metavar="RANGE",
action="append", type="string", dest="commits", default=[],
help="the commit (or range of commits) to test [default: current]")
parser.add_option("-v", "--verbose",
action="store_false", dest="quiet", default=True,
help="don't suppress overviewer output")
parser.add_option("-k", "--keep-going",
action="store_false", dest="fatal_errors", default=True,
help="don't stop testing when Overviewer croaks")
parser.add_option("-l", "--log", dest="log", default="", metavar="FILE",
help="log all test results to a file")
(options, args) = parser.parse_args()
if len(args) == 0:
parser.print_help()
sys.exit(0)
commits = []
for commit in options.commits:
if '..' in commit:
commits = get_commits(commit)
else:
commits.append(commit)
if not commits:
commits = [get_current_commit(),]
log = None
if options.log != "":
log = open(options.log, "w")
reset_commit = get_current_commit()
try:
for commit in commits:
print "testing commit", commit
set_commit(commit)
timelist = []
print " -- ",
try:
for i in range(options.number):
sys.stdout.write(str(i+1)+" ")
sys.stdout.flush()
timelist.append(clean_render(args, options.quiet))
print "... done"
stats = get_stats(timelist)
print stats
if log:
log.write("%s %s\n" % (commit, repr(stats)))
except CalledProcessError, e:
if options.fatal_errors:
print
print "Overviewer croaked, exiting..."
print "(to avoid this, use --keep-going)"
sys.exit(1)
finally:
set_commit(reset_commit)
if log:
log.close()

View File

@@ -0,0 +1,105 @@
#!/usr/bin/env python
import os.path
import sys
overviewer_dir = os.path.split(os.path.split(os.path.abspath(__file__))[0])[0]
sys.path.insert(0, overviewer_dir)
import nbt
def check_region(region_filename):
chunk_errors = []
if not os.path.exists(region_filename):
raise Exception('Region file not found: %s' % region_filename)
try:
region = nbt.load_region(region_filename)
except IOError, e:
raise Exception('Error loading region (%s): %s' % (region_filename, e))
try:
chunks = region.get_chunk_info(False)
except IOError, e:
raise Exception('Error reading region header (%s): %s' % (region_filename, e))
except Exception, e:
raise Exception('Error reading region (%s): %s' % (region_filename, e))
for x,y in chunks:
try:
check_chunk(region, x, y)
except Exception, e:
chunk_errors.append(e)
return (chunk_errors, len(chunks))
def check_chunk(region, x, y):
try:
data = region.load_chunk(x ,y)
except Exception, e:
raise Exception('Error reading chunk (%i, %i): %s' % (x, y, e))
if data is None:
raise Exception('Chunk (%i, %i) is unexpectedly empty' % (x, y))
else:
try:
processed_data = data.read_all()
except Exception, e:
raise Exception('Error reading chunk (%i, %i) data: %s' % (x, y, e))
if processed_data == []:
raise Exception('Chunk (%i, %i) is an unexpectedly empty set' % (x, y))
if __name__ == '__main__':
try:
from optparse import OptionParser
parser = OptionParser(usage='python contrib/%prog [OPTIONS] <path/to/regions|path/to/regions/*.mcr|regionfile1.mcr regionfile2.mcr ...>',
description='This script will valide a minecraft region file for errors.')
parser.add_option('-v', dest='verbose', action='store_true', help='Print additional information.')
opts, args = parser.parse_args()
region_files = []
for path in args:
if os.path.isdir(path):
for dirpath, dirnames, filenames in os.walk(path, True):
for filename in filenames:
if filename.startswith('r.') and filename.endswith('.mcr'):
if filename not in region_files:
region_files.append(os.path.join(dirpath, filename))
elif opts.verbose:
print('Ignoring non-region file: %s' % os.path.join(dirpath, filename))
elif os.path.isfile(path):
dirpath,filename = os.path.split(path)
if filename.startswith('r.') and filename.endswith('.mcr'):
if path not in region_files:
region_files.append(path)
else:
print('Ignoring non-region file: %s' % path)
else:
if opts.verbose:
print('Ignoring arg: %s' % path)
if len(region_files) < 1:
print 'You must list at least one region file.'
parser.print_help()
sys.exit(1)
else:
overall_chunk_total = 0
bad_chunk_total = 0
bad_region_total = 0
for region_file in region_files:
try:
(chunk_errors, region_chunks) = check_region(region_file)
bad_chunk_total += len(chunk_errors)
overall_chunk_total += region_chunks
except Exception, e:
bad_region_total += 1
print('FAILED(%s): %s' % (region_file, e))
else:
if len(chunk_errors) is not 0:
print('WARNING(%s) Chunks: %i/%' % (region_file, region_chunks - len(chunk_errors), region_chunks))
if opts.verbose:
for error in chunk_errors:
print(error)
elif opts.verbose:
print ('PASSED(%s) Chunks: %i/%i' % (region_file, region_chunks - len(chunk_errors), region_chunks))
if opts.verbose:
print 'REGIONS: %i/%i' % (len(region_files) - bad_region_total, len(region_files))
print 'CHUNKS: %i/%i' % (overall_chunk_total - bad_chunk_total, overall_chunk_total)
except KeyboardInterrupt:
sys.exit(1)
except Exception, e:
print('ERROR: %s' % e)

176
googlemap.py Normal file
View File

@@ -0,0 +1,176 @@
# 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 <http://www.gnu.org/licenses/>.
import os
import os.path
import stat
import cPickle
import Image
import shutil
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
interface out of a set of tiles.
"""
def mirror_dir(src, dst, entities=None):
'''copies all of the entities from src to dst'''
if not os.path.exists(dst):
os.mkdir(dst)
if entities and type(entities) != list: raise Exception("Expected a list, got a %r instead" % type(entities))
# files which are problematic and should not be copied
# usually, generated by the OS
skip_files = ['Thumbs.db', '.DS_Store']
for entry in os.listdir(src):
if entry in skip_files:
continue
if entities and entry not in entities:
continue
if os.path.isdir(os.path.join(src,entry)):
mirror_dir(os.path.join(src, entry), os.path.join(dst, entry))
elif os.path.isfile(os.path.join(src,entry)):
try:
shutil.copy(os.path.join(src, entry), os.path.join(dst, entry))
except IOError:
# maybe permission problems?
os.chmod(os.path.join(src, entry), stat.S_IRUSR)
os.chmod(os.path.join(dst, entry), stat.S_IWUSR)
shutil.copy(os.path.join(src, entry), os.path.join(dst, entry))
# if this stills throws an error, let it propagate up
class MapGen(object):
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 = 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.world = quadtrees[0].world
self.p = quadtrees[0].p
for i in quadtrees:
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
def go(self, procs):
"""Writes out config.js, marker.js, and region.js
Copies web assets into the destdir"""
zoomlevel = self.p
configpath = os.path.join(util.get_program_path(), "overviewerConfig.js")
config = open(configpath, 'r').read()
config = config.replace(
"{minzoom}", str(0))
config = config.replace(
"{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,
'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, "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:
tileDir = os.path.join(self.destdir, quadtree.tiledir)
if not os.path.exists(tileDir): os.mkdir(tileDir)
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)
# Add time in index.html
indexpath = os.path.join(self.destdir, "index.html")
index = open(indexpath, 'r').read()
index = index.replace(
"{time}", str(strftime("%a, %d %b %Y %H:%M:%S +0000", gmtime())))
with open(os.path.join(self.destdir, "index.html"), 'w') as output:
output.write(index)
if self.skipjs:
if self.web_assets_hook:
self.web_assets_hook(self)
return
def finalize(self):
if self.skipjs:
return
# since we will only discover PointsOfInterest in chunks that need to be
# [re]rendered, POIs like signs in unchanged chunks will not be listed
# in self.world.POI. To make sure we don't remove these from markers.js
# we need to merge self.world.POI with the persistant data in world.PersistentData
self.world.POI += filter(lambda x: x['type'] != 'spawn', self.world.persistentData['POI'])
# write out the default marker table
with open(os.path.join(self.destdir, "markers.js"), 'w') as output:
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")
# save persistent data
self.world.persistentData['POI'] = self.world.POI
with open(self.world.pickleFile,"wb") as f:
cPickle.dump(self.world.persistentData,f)
# write out the default (empty, but documented) region table
with open(os.path.join(self.destdir, "regions.js"), 'w') as output:
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(']);')

182
nbt.py
View File

@@ -26,7 +26,7 @@ def _file_loader(func):
return None
# Is actually a filename
fileobj = open(fileobj, 'rb')
fileobj = open(fileobj, 'rb',4096)
return func(fileobj, *args)
return wrapper
@@ -34,14 +34,30 @@ def _file_loader(func):
def load(fileobj):
return NBTFileReader(fileobj).read_all()
@_file_loader
def load_from_region(fileobj, x, y):
nbt = MCRFileReader(fileobj).load_chunk(x, y)
if not nbt:
def load_from_region(filename, x, y):
nbt = load_region(filename).load_chunk(x, y)
if nbt is None:
return None ## return none. I think this is who we should indicate missing chunks
#raise IOError("No such chunk in region: (%i, %i)" % (x, y))
#raise IOError("No such chunk in region: (%i, %i)" % (x, y))
return nbt.read_all()
def load_region(filename):
return MCRFileReader(filename)
# compile the unpacker's into a classes
_byte = struct.Struct("b")
_short = struct.Struct(">h")
_int = struct.Struct(">i")
_long = struct.Struct(">q")
_float = struct.Struct(">f")
_double = struct.Struct(">d")
_24bit_int = struct.Struct("B B B")
_unsigned_byte = struct.Struct("B")
_unsigned_int = struct.Struct(">I")
_chunk_header = struct.Struct(">I B")
class NBTFileReader(object):
def __init__(self, fileobj, is_gzip=True):
if is_gzip:
@@ -59,27 +75,32 @@ class NBTFileReader(object):
def _read_tag_byte(self):
byte = self._file.read(1)
return struct.unpack("b", byte)[0]
return _byte.unpack(byte)[0]
def _read_tag_short(self):
bytes = self._file.read(2)
return struct.unpack(">h", bytes)[0]
global _short
return _short.unpack(bytes)[0]
def _read_tag_int(self):
bytes = self._file.read(4)
return struct.unpack(">i", bytes)[0]
global _int
return _int.unpack(bytes)[0]
def _read_tag_long(self):
bytes = self._file.read(8)
return struct.unpack(">q", bytes)[0]
global _long
return _long.unpack(bytes)[0]
def _read_tag_float(self):
bytes = self._file.read(4)
return struct.unpack(">f", bytes)[0]
global _float
return _float.unpack(bytes)[0]
def _read_tag_double(self):
bytes = self._file.read(8)
return struct.unpack(">d", bytes)[0]
global _double
return _double.unpack(bytes)[0]
def _read_tag_byte_array(self):
length = self._read_tag_int()
@@ -178,9 +199,9 @@ class MCRFileReader(object):
chunks (as instances of NBTFileReader), getting chunk timestamps,
and for listing chunks contained in the file."""
def __init__(self, fileobj):
self._file = fileobj
def __init__(self, filename):
self._file = None
self._filename = filename
# cache used when the entire header tables are read in get_chunks()
self._locations = None
self._timestamps = None
@@ -192,9 +213,11 @@ class MCRFileReader(object):
ret = 0
bytes = self._file.read(3)
global _24bit_int
bytes = _24bit_int.unpack(bytes)
for i in xrange(3):
ret = ret << 8
ret += struct.unpack("B", bytes[i])[0]
ret += bytes[i]
return ret
@@ -204,7 +227,7 @@ class MCRFileReader(object):
and y must be between 0 and 31, or None. If they are None,
then there will be no file seek before doing the read."""
if x != None and y != None:
if x is not None and y is not None:
if (not x >= 0) or (not x < 32) or (not y >= 0) or (not y < 32):
raise ValueError("Chunk location out of range.")
@@ -215,12 +238,17 @@ class MCRFileReader(object):
# go to the correct entry in the chunk location table
self._file.seek(4 * (x + y * 32))
# 3-byte offset in 4KiB sectors
offset_sectors = self._read_24bit_int()
# 1-byte length in 4KiB sectors, rounded up
byte = self._file.read(1)
length_sectors = struct.unpack("B", byte)[0]
try:
# 3-byte offset in 4KiB sectors
offset_sectors = self._read_24bit_int()
# 1-byte length in 4KiB sectors, rounded up
global _unsigned_byte
byte = self._file.read(1)
length_sectors = _unsigned_byte.unpack(byte)[0]
except (IndexError, struct.error):
# got a problem somewhere
return None
# check for empty chunks
if offset_sectors == 0 or length_sectors == 0:
@@ -234,7 +262,7 @@ class MCRFileReader(object):
None. If they are, None, then there will be no file seek
before doing the read."""
if x != None and y != None:
if x is not None and y is not None:
if (not x >= 0) or (not x < 32) or (not y >= 0) or (not y < 32):
raise ValueError("Chunk location out of range.")
@@ -245,70 +273,115 @@ class MCRFileReader(object):
# go to the correct entry in the chunk timestamp table
self._file.seek(4 * (x + y * 32) + 4096)
bytes = self._file.read(4)
timestamp = struct.unpack(">I", bytes)[0]
try:
bytes = self._file.read(4)
global _unsigned_int
timestamp = _unsigned_int.unpack(bytes)[0]
except (IndexError, struct.error):
return 0
return timestamp
def get_chunks(self):
def openfile(self):
#make sure we clean up
if self._file is None:
self._file = open(self._filename,'rb')
def closefile(self):
#make sure we clean up
if self._file is not None:
self._file.close()
self._file = None
def get_chunks(self):
"""Return a list of all chunks contained in this region file,
as a list of (x, y) coordinate tuples. To load these chunks,
provide these coordinates to load_chunk()."""
if self._chunks:
if self._chunks is not None:
return self._chunks
if self._locations is None:
self.get_chunk_info()
self._chunks = []
for x in xrange(32):
for y in xrange(32):
if self._locations[x + y * 32] is not None:
self._chunks.append((x,y))
return self._chunks
self._chunks = []
def get_chunk_info(self,closeFile = True):
"""Preloads region header information."""
if self._locations:
return
self.openfile()
self._chunks = None
self._locations = []
self._timestamps = []
# go to the beginning of the file
self._file.seek(0)
self._file.seek(0)
# read chunk location table
for y in xrange(32):
for x in xrange(32):
location = self._read_chunk_location()
self._locations.append(location)
if location:
self._chunks.append((x, y))
locations_append = self._locations.append
for _ in xrange(32*32):
locations_append(self._read_chunk_location())
# read chunk timestamp table
for y in xrange(32):
for x in xrange(32):
timestamp = self._read_chunk_timestamp()
self._timestamps.append(timestamp)
return self._chunks
timestamp_append = self._timestamps.append
for _ in xrange(32*32):
timestamp_append(self._read_chunk_timestamp())
if closeFile:
self.closefile()
return
def get_chunk_timestamp(self, x, y):
"""Return the given chunk's modification time. If the given
chunk doesn't exist, this number may be nonsense. Like
load_chunk(), this will wrap x and y into the range [0, 31].
"""
return self._read_chunk_timestamp(x % 32, y % 32)
x = x % 32
y = y % 32
if self._timestamps is None:
self.get_chunk_info()
return self._timestamps[x + y * 32]
def load_chunk(self, x, y):
def chunkExists(self, x, y):
"""Determines if a chunk exists without triggering loading of the backend data"""
x = x % 32
y = y % 32
if self._locations is None:
self.get_chunk_info()
location = self._locations[x + y * 32]
return location is not None
def load_chunk(self, x, y,closeFile=True):
"""Return a NBTFileReader instance for the given chunk, or
None if the given chunk doesn't exist in this region file. If
you provide an x or y not between 0 and 31, it will be
modulo'd into this range (x % 32, etc.) This is so you can
provide chunk coordinates in global coordinates, and still
have the chunks load out of regions properly."""
location = self._read_chunk_location(x % 32, y % 32)
if not location:
x = x % 32
y = y % 32
if self._locations is None:
self.get_chunk_info()
location = self._locations[x + y * 32]
if location is None:
return None
self.openfile()
# seek to the data
self._file.seek(location[0])
# read in the chunk data header
bytes = self._file.read(4)
data_length = struct.unpack(">I", bytes)[0]
bytes = self._file.read(1)
compression = struct.unpack("B", bytes)[0]
bytes = self._file.read(5)
data_length,compression = _chunk_header.unpack(bytes)
# figure out the compression
is_gzip = True
@@ -320,11 +393,12 @@ class MCRFileReader(object):
is_gzip = False
else:
# unsupported!
raise Exception("Unsupported chunk compression type: %i" % (compression,))
raise Exception("Unsupported chunk compression type: %i" % (compression))
# turn the rest of the data into a StringIO object
# (using data_length - 1, as we already read 1 byte for compression)
data = self._file.read(data_length - 1)
data = StringIO.StringIO(data)
if closeFile:
self.closefile()
return NBTFileReader(data, is_gzip=is_gzip)

View File

@@ -22,49 +22,114 @@ if not (sys.version_info[0] == 2 and sys.version_info[1] >= 6):
import os
import os.path
from optparse import OptionParser
from configParser import ConfigOptionParser
import re
import subprocess
import multiprocessing
import time
import logging
import optimizeimages
import composite
import util
import platform
logging.basicConfig(level=logging.INFO,format="%(asctime)s [%(levelname)s] %(message)s")
this_dir = util.get_program_path()
# make sure the c_overviewer extension is available
try:
import c_overviewer
except ImportError:
## try to find the build extension
ext = os.path.join(this_dir, "c_overviewer.%s" % ("pyd" if platform.system() == "Windows" else "so"))
if os.path.exists(ext):
print "Something has gone wrong importing the c_overviewer extension. Please"
print "make sure it is up-to-date (clean and rebuild)"
sys.exit(1)
print "You need to compile the c_overviewer module to run Minecraft Overviewer."
print "Run `python setup.py build`, or see the README for details."
sys.exit(1)
if hasattr(sys, "frozen"):
pass # we don't bother with a compat test since it should always be in sync
elif "extension_version" in dir(c_overviewer):
# check to make sure the binary matches the headers
if os.path.exists(os.path.join(this_dir, "src", "overviewer.h")):
with open(os.path.join(this_dir, "src", "overviewer.h")) as f:
lines = f.readlines()
lines = filter(lambda x: x.startswith("#define OVERVIEWER_EXTENSION_VERSION"), lines)
if lines:
l = lines[0]
if int(l.split()[2].strip()) != c_overviewer.extension_version():
print "Please rebuild your c_overviewer module. It is out of date!"
sys.exit(1)
else:
print "Please rebuild your c_overviewer module. It is out of date!"
sys.exit(1)
import optimizeimages
import world
import quadtree
import googlemap
import rendernode
helptext = """
%prog [OPTIONS] <World # / Name / Path to World> <tiles dest dir>
%prog -d <World # / Name / Path to World / Path to cache dir> [tiles dest dir]"""
def main():
try:
cpus = multiprocessing.cpu_count()
except NotImplementedError:
cpus = 1
parser = OptionParser(usage=helptext)
avail_rendermodes = c_overviewer.get_render_modes()
parser = ConfigOptionParser(usage=helptext, config="settings.py")
parser.add_option("-V", "--version", dest="version", help="Displays version information and then exits", action="store_true")
parser.add_option("-p", "--processes", dest="procs", help="How many worker processes to start. Default %s" % cpus, default=cpus, action="store", type="int")
parser.add_option("-z", "--zoom", dest="zoom", help="Sets the zoom level manually instead of calculating it. This can be useful if you have outlier chunks that make your world too big. This value will make the highest zoom level contain (2**ZOOM)^2 tiles", action="store", type="int")
parser.add_option("-d", "--delete", dest="delete", help="Clear all caches. Next time you render your world, it will have to start completely over again. This is probably not a good idea for large worlds. Use this if you change texture packs and want to re-render everything.", action="store_true")
parser.add_option("--cachedir", dest="cachedir", help="Sets the directory where the Overviewer will save chunk images, which is an intermediate step before the tiles are generated. You must use the same directory each time to gain any benefit from the cache. If not set, this defaults to your world directory.")
parser.add_option("-z", "--zoom", dest="zoom", help="Sets the zoom level manually instead of calculating it. This can be useful if you have outlier chunks that make your world too big. This value will make the highest zoom level contain (2**ZOOM)^2 tiles", action="store", type="int", configFileOnly=True)
parser.add_option("-d", "--delete", dest="delete", help="Clear all caches. Next time you render your world, it will have to start completely over again. This is probably not a good idea for large worlds. Use this if you change texture packs and want to re-render everything.", action="store_true", commandLineOnly=True)
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("--lighting", dest="lighting", help="Renders shadows using light data from each chunk.", action="store_true")
parser.add_option("--night", dest="night", help="Renders shadows using light data from each chunk, as if it were night. Implies --lighting.", action="store_true")
parser.add_option("--spawn", dest="spawn", help="Renders shadows using light data from each chunk, as if it were night, while also highlighting areas that are dark enough to spawn mobs. Implies --lighting and --night.", action="store_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.")
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%")
parser.add_option("--web-assets-hook", dest="web_assets_hook", help="If provided, run this script after the web assets have been copied, but before actual tile rendering begins. See the README for details.", action="store", metavar="SCRIPT", type="string")
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("--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.")
parser.add_option("-v", "--verbose", dest="verbose", action="count", default=0, help="Print more output. You can specify this option multiple times.")
parser.add_option("--skip-js", dest="skipjs", action="store_true", help="Don't output marker.js or regions.js")
parser.add_option("--display-config", dest="display_config", action="store_true", help="Display the configuration parameters, but don't render the map. Requires all required options to be specified", commandLineOnly=True)
#parser.add_option("--write-config", dest="write_config", action="store_true", help="Writes out a sample config file", commandLineOnly=True)
options, args = parser.parse_args()
if options.version:
print "Minecraft-Overviewer"
print "Git version: %s" % util.findGitVersion()
try:
import overviewer_version
if hasattr(sys, "frozen"):
print "py2exe version build on %s" % overviewer_version.BUILD_DATE
print "Build machine: %s %s" % (overviewer_version.BUILD_PLATFORM, overviewer_version.BUILD_OS)
except:
pass
sys.exit(0)
if options.list_rendermodes:
rendermode_info = map(c_overviewer.get_render_mode_info, avail_rendermodes)
name_width = max(map(lambda i: len(i['name']), rendermode_info))
for info in rendermode_info:
print "{name:{0}} {description}".format(name_width, **info)
sys.exit(0)
if len(args) < 1:
print "You need to give me your world number or directory"
logging.error("You need to give me your world number or directory")
parser.print_help()
list_worlds()
sys.exit(1)
@@ -77,7 +142,7 @@ def main():
# if there are no worlds found at all, exit now
if not worlds:
parser.print_help()
print "\nInvalid world path"
logging.error("Invalid world path")
sys.exit(1)
try:
@@ -90,28 +155,28 @@ def main():
except KeyError:
# it's not a number, name, or path
parser.print_help()
print "Invalid world name or path"
logging.error("Invalid world name or path")
sys.exit(1)
except KeyError:
# it was an invalid number
parser.print_help()
print "Invalid world number"
logging.error("Invalid world number")
sys.exit(1)
if not options.cachedir:
cachedir = worlddir
else:
cachedir = options.cachedir
if len(args) != 2:
if options.delete:
return delete_all(cachedir, None)
return delete_all(worlddir, None)
parser.error("Where do you want to save the tiles?")
destdir = args[1]
if options.display_config:
# just display the config file and exit
parser.display_config()
sys.exit(0)
if options.delete:
return delete_all(cachedir, destdir)
return delete_all(worlddir, destdir)
if options.chunklist:
chunklist = open(options.chunklist, 'r')
@@ -132,21 +197,6 @@ def main():
else:
optimizeimg = None
if options.web_assets_hook:
if not os.path.exists(options.web_assets_hook):
parser.error("Provided hook script does not exist!")
def web_assets_hook(quadtree):
if options.web_assets_hook == None:
return
try:
subprocess.check_call((options.web_assets_hook, os.path.abspath(quadtree.destdir)))
except OSError, e:
logging.error("could not call web assets hook: %s" % (e,))
sys.exit(1)
except subprocess.CalledProcessError:
logging.error("web assets hook returned error")
sys.exit(1)
logging.getLogger().setLevel(
logging.getLogger().level + 10*options.quiet)
logging.getLogger().setLevel(
@@ -154,45 +204,51 @@ def main():
logging.info("Welcome to Minecraft Overviewer!")
logging.debug("Current log level: {0}".format(logging.getLogger().level))
if not composite.extension_alpha_over:
logging.info("Notice: alpha_over extension not found; using default PIL paste()")
useBiomeData = os.path.exists(os.path.join(worlddir, 'biomes'))
if not useBiomeData:
logging.info("Notice: Not using biome data for tinting")
# First generate the world's chunk images
w = world.WorldRenderer(worlddir, cachedir, chunklist=chunklist, lighting=options.lighting, night=options.night, spawn=options.spawn, useBiomeData=useBiomeData)
# First do world-level preprocessing
w = world.World(worlddir, useBiomeData=useBiomeData)
w.go(options.procs)
# Now generate the tiles
q = quadtree.QuadtreeGen(w, destdir, depth=options.zoom, imgformat=imgformat, optimizeimg=optimizeimg, web_assets_hook=web_assets_hook)
q.write_html(options.skipjs)
q.go(options.procs)
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, '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
r = rendernode.RenderNode(q)
# write out the map and web assets
m = googlemap.MapGen(q, configInfo=options)
m.go(options.procs)
# render the tiles!
r.go(options.procs)
# finish up the map
m.finalize()
def delete_all(worlddir, tiledir):
# First delete all images in the world dir
imgre = r"img\.[^.]+\.[^.]+\.nocave\.\w+\.png$"
matcher = re.compile(imgre)
for dirpath, dirnames, filenames in os.walk(worlddir):
for f in filenames:
if matcher.match(f):
filepath = os.path.join(dirpath, f)
logging.info("Deleting {0}".format(filepath))
os.unlink(filepath)
# Now delete all /hash/ files in the tile dir.
if tiledir:
for dirpath, dirnames, filenames in os.walk(tiledir):
for f in filenames:
if f.endswith(".hash"):
filepath = os.path.join(dirpath, f)
logging.info("Deleting {0}".format(filepath))
os.unlink(filepath)
# TODO should we delete tiledir here too?
# delete the overviewer.dat persistant data file
datfile = os.path.join(worlddir,"overviewer.dat")
if os.path.exists(datfile):

157
overviewerConfig.js Normal file
View File

@@ -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}
};

View File

@@ -17,7 +17,6 @@ import multiprocessing
import itertools
import os
import os.path
import hashlib
import functools
import re
import shutil
@@ -27,10 +26,15 @@ import logging
import util
import cPickle
import stat
import errno
import time
from time import gmtime, strftime, sleep
from PIL import Image
import nbt
import chunk
from c_overviewer import get_render_mode_inheritance
from optimizeimages import optimize_image
import composite
@@ -40,48 +44,12 @@ This module has routines related to generating a quadtree of tiles
"""
def mirror_dir(src, dst, entities=None):
'''copies all of the entities from src to dst'''
if not os.path.exists(dst):
os.mkdir(dst)
if entities and type(entities) != list: raise Exception("Expected a list, got a %r instead" % type(entities))
for entry in os.listdir(src):
if entities and entry not in entities: continue
if os.path.isdir(os.path.join(src,entry)):
mirror_dir(os.path.join(src, entry), os.path.join(dst, entry))
elif os.path.isfile(os.path.join(src,entry)):
try:
shutil.copy(os.path.join(src, entry), os.path.join(dst, entry))
except IOError:
# maybe permission problems?
os.chmod(os.path.join(src, entry), stat.S_IRUSR)
os.chmod(os.path.join(dst, entry), stat.S_IWUSR)
shutil.copy(os.path.join(src, entry), os.path.join(dst, entry))
# if this stills throws an error, let it propagate up
def iterate_base4(d):
"""Iterates over a base 4 number with d digits"""
return itertools.product(xrange(4), repeat=d)
def catch_keyboardinterrupt(func):
"""Decorator that catches a keyboardinterrupt and raises a real exception
so that multiprocessing will propagate it properly"""
@functools.wraps(func)
def newfunc(*args, **kwargs):
try:
return func(*args, **kwargs)
except KeyboardInterrupt:
logging.error("Ctrl-C caught!")
raise Exception("Exiting")
except:
import traceback
traceback.print_exc()
raise
return newfunc
class QuadtreeGen(object):
def __init__(self, worldobj, destdir, depth=None, imgformat=None, optimizeimg=None, web_assets_hook=None):
def __init__(self, worldobj, destdir, bgcolor, depth=None, tiledir=None, imgformat=None, optimizeimg=None, rendermode="normal"):
"""Generates a quadtree from the world given into the
given dest directory
@@ -94,12 +62,20 @@ class QuadtreeGen(object):
assert(imgformat)
self.imgformat = imgformat
self.optimizeimg = optimizeimg
self.web_assets_hook = web_assets_hook
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
if depth is None:
# Determine quadtree depth (midpoint is always 0,0)
for p in xrange(15):
@@ -131,90 +107,7 @@ class QuadtreeGen(object):
self.world = worldobj
self.destdir = destdir
def print_statusline(self, complete, total, level, unconditional=False):
if unconditional:
pass
elif complete < 100:
if not complete % 25 == 0:
return
elif complete < 1000:
if not complete % 100 == 0:
return
else:
if not complete % 1000 == 0:
return
logging.info("{0}/{1} tiles complete on level {2}/{3}".format(
complete, total, level, self.p))
def write_html(self, skipjs=False):
"""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")
config = open(configpath, 'r').read()
config = config.replace(
"{maxzoom}", str(zoomlevel))
config = config.replace(
"{imgformat}", str(imgformat))
with open(os.path.join(self.destdir, "config.js"), 'w') as output:
output.write(config)
# Write a blank image
blank = Image.new("RGBA", (1,1))
tileDir = os.path.join(self.destdir, "tiles")
if not os.path.exists(tileDir): os.mkdir(tileDir)
blank.save(os.path.join(tileDir, "blank."+self.imgformat))
# copy web assets into destdir:
mirror_dir(os.path.join(util.get_program_path(), "web_assets"), self.destdir)
# Add time in index.html
indexpath = os.path.join(self.destdir, "index.html")
index = open(indexpath, 'r').read()
index = index.replace(
"{time}", str(strftime("%a, %d %b %Y %H:%M:%S +0000", gmtime())))
with open(os.path.join(self.destdir, "index.html"), 'w') as output:
output.write(index)
if skipjs:
if self.web_assets_hook:
self.web_assets_hook(self)
return
# since we will only discover PointsOfInterest in chunks that need to be
# [re]rendered, POIs like signs in unchanged chunks will not be listed
# in self.world.POI. To make sure we don't remove these from markers.js
# we need to merge self.world.POI with the persistant data in world.PersistentData
self.world.POI += filter(lambda x: x['type'] != 'spawn', self.world.persistentData['POI'])
# write out the default marker table
with open(os.path.join(self.destdir, "markers.js"), 'w') as output:
output.write("var markerData=%s" % json.dumps(self.world.POI))
# save persistent data
self.world.persistentData['POI'] = self.world.POI
with open(self.world.pickleFile,"wb") as f:
cPickle.dump(self.world.persistentData,f)
# 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(' // {"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('];')
if self.web_assets_hook:
self.web_assets_hook(self)
self.full_tiledir = os.path.join(destdir, tiledir)
def _get_cur_depth(self):
"""How deep is the quadtree currently in the destdir? This glances in
@@ -222,10 +115,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)
@@ -236,7 +129,7 @@ class QuadtreeGen(object):
def _increase_depth(self):
"""Moves existing tiles into place for a larger tree"""
getpath = functools.partial(os.path.join, self.destdir, "tiles")
getpath = functools.partial(os.path.join, self.destdir, self.tiledir)
# At top level of the tree:
# quadrant 0 is now 0/3
@@ -250,8 +143,8 @@ class QuadtreeGen(object):
newdir = "new" + str(dirnum)
newdirpath = getpath(newdir)
files = [str(dirnum)+"."+self.imgformat, str(dirnum)+".hash", str(dirnum)]
newfiles = [str(newnum)+"."+self.imgformat, str(newnum)+".hash", str(newnum)]
files = [str(dirnum)+"."+self.imgformat, str(dirnum)]
newfiles = [str(newnum)+"."+self.imgformat, str(newnum)]
os.mkdir(newdirpath)
for f, newf in zip(files, newfiles):
@@ -263,7 +156,7 @@ class QuadtreeGen(object):
def _decrease_depth(self):
"""If the map size decreases, or perhaps the user has a depth override
in effect, re-arrange existing tiles for a smaller tree"""
getpath = functools.partial(os.path.join, self.destdir, "tiles")
getpath = functools.partial(os.path.join, self.destdir, self.tiledir)
# quadrant 0/3 goes to 0
# 1/2 goes to 1
@@ -290,47 +183,9 @@ class QuadtreeGen(object):
os.rename(getpath("3", "0"), getpath("new3"))
shutil.rmtree(getpath("3"))
os.rename(getpath("new3"), getpath("3"))
def _apply_render_worldtiles(self, pool):
"""Returns an iterator over result objects. Each time a new result is
requested, a new task is added to the pool and a result returned.
"""
for path in iterate_base4(self.p):
# Get the range for this tile
colstart, rowstart = self._get_range_by_path(path)
colend = colstart + 2
rowend = rowstart + 4
# This image is rendered at:
dest = os.path.join(self.destdir, "tiles", *(str(x) for x in path))
#logging.debug("this is rendered at %s", dest)
# And uses these chunks
tilechunks = self._get_chunks_in_range(colstart, colend, rowstart,
rowend)
#logging.debug(" tilechunks: %r", tilechunks)
# Put this in the pool
# (even if tilechunks is empty, render_worldtile will delete
# existing images if appropriate)
yield pool.apply_async(func=render_worldtile, args= (tilechunks,
colstart, colend, rowstart, rowend, dest, self.imgformat,
self.optimizeimg))
def _apply_render_inntertile(self, pool, zoom):
"""Same as _apply_render_worltiles but for the inntertile routine.
Returns an iterator that yields result objects from tasks that have
been applied to the pool.
"""
for path in iterate_base4(zoom):
# This image is rendered at:
dest = os.path.join(self.destdir, "tiles", *(str(x) for x in path[:-1]))
name = str(path[-1])
yield pool.apply_async(func=render_innertile, args= (dest, name, self.imgformat, self.optimizeimg))
def go(self, procs):
"""Renders all tiles"""
"""Processing before tile rendering"""
curdepth = self._get_cur_depth()
if curdepth != -1:
@@ -343,70 +198,8 @@ class QuadtreeGen(object):
logging.warning("Your map seems to have shrunk. Re-arranging tiles, just a sec...")
for _ in xrange(curdepth - self.p):
self._decrease_depth()
# Create a pool
if procs == 1:
pool = FakePool()
else:
pool = multiprocessing.Pool(processes=procs)
# Render the highest level of tiles from the chunks
results = collections.deque()
complete = 0
total = 4**self.p
logging.info("Rendering highest zoom level of tiles now.")
logging.info("There are {0} tiles to render".format(total))
logging.info("There are {0} total levels to render".format(self.p))
logging.info("Don't worry, each level has only 25% as many tiles as the last.")
logging.info("The others will go faster")
for result in self._apply_render_worldtiles(pool):
results.append(result)
if len(results) > 10000:
# Empty the queue before adding any more, so that memory
# required has an upper bound
while len(results) > 500:
results.popleft().get()
complete += 1
self.print_statusline(complete, total, 1)
# Wait for the rest of the results
while len(results) > 0:
results.popleft().get()
complete += 1
self.print_statusline(complete, total, 1)
self.print_statusline(complete, total, 1, True)
# Now do the other layers
for zoom in xrange(self.p-1, 0, -1):
level = self.p - zoom + 1
assert len(results) == 0
complete = 0
total = 4**zoom
logging.info("Starting level {0}".format(level))
for result in self._apply_render_inntertile(pool, zoom):
results.append(result)
if len(results) > 10000:
while len(results) > 500:
results.popleft().get()
complete += 1
self.print_statusline(complete, total, level)
# Empty the queue
while len(results) > 0:
results.popleft().get()
complete += 1
self.print_statusline(complete, total, level)
self.print_statusline(complete, total, level, True)
logging.info("Done")
pool.close()
pool.join()
# Do the final one right here:
render_innertile(os.path.join(self.destdir, "tiles"), "base", self.imgformat, self.optimizeimg)
def _get_range_by_path(self, path):
"""Returns the x, y chunk coordinates of this tile"""
x, y = self.mincol, self.minrow
@@ -423,282 +216,254 @@ class QuadtreeGen(object):
ysize //= 2
return x, y
def _get_chunks_in_range(self, colstart, colend, rowstart, rowend):
def get_chunks_in_range(self, colstart, colend, rowstart, rowend):
"""Get chunks that are relevant to the tile rendering function that's
rendering that range"""
chunklist = []
unconvert_coords = self.world.unconvert_coords
#get_region_path = self.world.get_region_path
get_region = self.world.regionfiles.get
regionx = None
regiony = None
c = None
mcr = None
for row in xrange(rowstart-16, rowend+1):
for col in xrange(colstart, colend+1):
c = self.world.chunkmap.get((col, row), None)
if c:
chunklist.append((col, row, c))
return chunklist
# due to how chunks are arranged, we can only allow
# even row, even column or odd row, odd column
# otherwise, you end up with duplicates!
if row % 2 != col % 2:
continue
chunkx, chunky = unconvert_coords(col, row)
@catch_keyboardinterrupt
def render_innertile(dest, name, imgformat, optimizeimg):
"""
Renders a tile at os.path.join(dest, name)+".ext" by taking tiles from
os.path.join(dest, name, "{0,1,2,3}.png")
"""
imgpath = os.path.join(dest, name) + "." + imgformat
hashpath = os.path.join(dest, name) + ".hash"
if name == "base":
q0path = os.path.join(dest, "0." + imgformat)
q1path = os.path.join(dest, "1." + imgformat)
q2path = os.path.join(dest, "2." + imgformat)
q3path = os.path.join(dest, "3." + imgformat)
q0hash = os.path.join(dest, "0.hash")
q1hash = os.path.join(dest, "1.hash")
q2hash = os.path.join(dest, "2.hash")
q3hash = os.path.join(dest, "3.hash")
else:
q0path = os.path.join(dest, name, "0." + imgformat)
q1path = os.path.join(dest, name, "1." + imgformat)
q2path = os.path.join(dest, name, "2." + imgformat)
q3path = os.path.join(dest, name, "3." + imgformat)
q0hash = os.path.join(dest, name, "0.hash")
q1hash = os.path.join(dest, name, "1.hash")
q2hash = os.path.join(dest, name, "2.hash")
q3hash = os.path.join(dest, name, "3.hash")
# Check which ones exist
if not os.path.exists(q0hash):
q0path = None
q0hash = None
if not os.path.exists(q1hash):
q1path = None
q1hash = None
if not os.path.exists(q2hash):
q2path = None
q2hash = None
if not os.path.exists(q3hash):
q3path = None
q3hash = None
# do they all not exist?
if not (q0path or q1path or q2path or q3path):
if os.path.exists(imgpath):
os.unlink(imgpath)
if os.path.exists(hashpath):
os.unlink(hashpath)
return
regionx_ = chunkx//32
regiony_ = chunky//32
if regionx_ != regionx or regiony_ != regiony:
regionx = regionx_
regiony = regiony_
_, _, c, mcr = get_region((regionx, regiony),(None,None,None,None))
if c is not None and mcr.chunkExists(chunkx,chunky):
chunklist.append((col, row, chunkx, chunky, c))
return chunklist
def get_worldtiles(self):
"""Returns an iterator over the tiles of the most detailed layer
"""
for path in iterate_base4(self.p):
# Get the range for this tile
colstart, rowstart = self._get_range_by_path(path)
colend = colstart + 2
rowend = rowstart + 4
# This image is rendered at(relative to the worker's destdir):
tilepath = [str(x) for x in path]
tilepath = os.sep.join(tilepath)
#logging.debug("this is rendered at %s", dest)
# Put this in the batch to be submited to the pool
yield [self,colstart, colend, rowstart, rowend, tilepath]
def get_innertiles(self,zoom):
"""Same as get_worldtiles but for the inntertile routine.
"""
for path in iterate_base4(zoom):
# This image is rendered at(relative to the worker's destdir):
tilepath = [str(x) for x in path[:-1]]
tilepath = os.sep.join(tilepath)
name = str(path[-1])
yield [self,tilepath, name]
# Now check the hashes
hasher = hashlib.md5()
if q0hash:
hasher.update(open(q0hash, "rb").read())
if q1hash:
hasher.update(open(q1hash, "rb").read())
if q2hash:
hasher.update(open(q2hash, "rb").read())
if q3hash:
hasher.update(open(q3hash, "rb").read())
if os.path.exists(hashpath):
oldhash = open(hashpath, "rb").read()
else:
oldhash = None
newhash = hasher.digest()
def render_innertile(self, dest, name):
"""
Renders a tile at os.path.join(dest, name)+".ext" by taking tiles from
os.path.join(dest, name, "{0,1,2,3}.png")
"""
imgformat = self.imgformat
imgpath = os.path.join(dest, name) + "." + imgformat
if newhash == oldhash:
# Nothing to do
return
# Create the actual image now
img = Image.new("RGBA", (384, 384), (38,92,255,0))
# we'll use paste (NOT alpha_over) for quadtree generation because
# this is just straight image stitching, not alpha blending
if q0path:
try:
quad0 = Image.open(q0path).resize((192,192), Image.ANTIALIAS)
img.paste(quad0, (0,0))
except Exception, e:
logging.warning("Couldn't open %s. It may be corrupt, you may need to delete it. %s", q0path, e)
if q1path:
try:
quad1 = Image.open(q1path).resize((192,192), Image.ANTIALIAS)
img.paste(quad1, (192,0))
except Exception, e:
logging.warning("Couldn't open %s. It may be corrupt, you may need to delete it. %s", q1path, e)
if q2path:
try:
quad2 = Image.open(q2path).resize((192,192), Image.ANTIALIAS)
img.paste(quad2, (0, 192))
except Exception, e:
logging.warning("Couldn't open %s. It may be corrupt, you may need to delete it. %s", q2path, e)
if q3path:
try:
quad3 = Image.open(q3path).resize((192,192), Image.ANTIALIAS)
img.paste(quad3, (192, 192))
except Exception, e:
logging.warning("Couldn't open %s. It may be corrupt, you may need to delete it. %s", q3path, e)
# Save it
if imgformat == 'jpg':
img.save(imgpath, quality=95, subsampling=0)
else: # png
img.save(imgpath)
if optimizeimg:
optimize_image(imgpath, imgformat, optimizeimg)
with open(hashpath, "wb") as hashout:
hashout.write(newhash)
@catch_keyboardinterrupt
def render_worldtile(chunks, colstart, colend, rowstart, rowend, path, imgformat, optimizeimg):
"""Renders just the specified chunks into a tile and save it. Unlike usual
python conventions, rowend and colend are inclusive. Additionally, the
chunks around the edges are half-way cut off (so that neighboring tiles
will render the other half)
chunks is a list of (col, row, filename) of chunk images that are relevant
to this call
The image is saved to path+".ext" and a hash is saved to path+".hash"
If there are no chunks, this tile is not saved (if it already exists, it is
deleted)
If the hash file already exists, it is checked against the hash of each chunk.
Standard tile size has colend-colstart=2 and rowend-rowstart=4
There is no return value
"""
# width of one chunk is 384. Each column is half a chunk wide. The total
# width is (384 + 192*(numcols-1)) since the first column contributes full
# width, and each additional one contributes half since they're staggered.
# However, since we want to cut off half a chunk at each end (384 less
# pixels) and since (colend - colstart + 1) is the number of columns
# inclusive, the equation simplifies to:
width = 192 * (colend - colstart)
# Same deal with height
height = 96 * (rowend - rowstart)
# The standard tile size is 3 columns by 5 rows, which works out to 384x384
# pixels for 8 total chunks. (Since the chunks are staggered but the grid
# is not, some grid coordinates do not address chunks) The two chunks on
# the middle column are shown in full, the two chunks in the middle row are
# half cut off, and the four remaining chunks are one quarter shown.
# The above example with cols 0-3 and rows 0-4 has the chunks arranged like this:
# 0,0 2,0
# 1,1
# 0,2 2,2
# 1,3
# 0,4 2,4
# Due to how the tiles fit together, we may need to render chunks way above
# this (since very few chunks actually touch the top of the sky, some tiles
# way above this one are possibly visible in this tile). Render them
# anyways just in case). "chunks" should include up to rowstart-16
# Before we render any tiles, check the hash of each image in this tile to
# see if it's changed.
hashpath = path + ".hash"
imgpath = path + "." + imgformat
if not chunks:
# No chunks were found in this tile
if os.path.exists(imgpath):
os.unlink(imgpath)
if os.path.exists(hashpath):
os.unlink(hashpath)
return None
# Create the directory if not exists
dirdest = os.path.dirname(path)
if not os.path.exists(dirdest):
try:
os.makedirs(dirdest)
if name == "base":
quadPath = [[(0,0),os.path.join(dest, "0." + imgformat)],[(192,0),os.path.join(dest, "1." + imgformat)], [(0, 192),os.path.join(dest, "2." + imgformat)],[(192,192),os.path.join(dest, "3." + imgformat)]]
else:
quadPath = [[(0,0),os.path.join(dest, name, "0." + imgformat)],[(192,0),os.path.join(dest, name, "1." + imgformat)],[(0, 192),os.path.join(dest, name, "2." + imgformat)],[(192,192),os.path.join(dest, name, "3." + imgformat)]]
#stat the tile, we need to know if it exists or it's mtime
try:
tile_mtime = os.stat(imgpath)[stat.ST_MTIME];
except OSError, e:
# Ignore errno EEXIST: file exists. Since this is multithreaded,
# two processes could conceivably try and create the same directory
# at the same time.
import errno
if e.errno != errno.EEXIST:
if e.errno != errno.ENOENT:
raise
imghash = hashlib.md5()
for col, row, chunkfile in chunks:
# Get the hash of this image and add it to our hash for this tile
imghash.update(
os.path.basename(chunkfile).split(".")[4]
)
digest = imghash.digest()
if os.path.exists(hashpath):
oldhash = open(hashpath, 'rb').read()
else:
oldhash = None
if digest == oldhash:
# All the chunks for this tile have not changed according to the hash
return
# Compile this image
tileimg = Image.new("RGBA", (width, height), (38,92,255,0))
# col colstart will get drawn on the image starting at x coordinates -(384/2)
# row rowstart will get drawn on the image starting at y coordinates -(192/2)
for col, row, chunkfile in chunks:
try:
chunkimg = Image.open(chunkfile)
chunkimg.load()
except Exception, e:
# If for some reason the chunk failed to load (perhaps a previous
# run was canceled and the file was only written half way,
# corrupting it), then this could error.
# Since we have no easy way of determining how this chunk was
# generated, we need to just ignore it.
logging.warning("Could not open chunk '{0}' ({1})".format(chunkfile,e))
tile_mtime = None
#check mtimes on each part of the quad, this also checks if they exist
needs_rerender = tile_mtime is None
quadPath_filtered = []
for path in quadPath:
try:
# Remove the file so that the next run will re-generate it.
os.unlink(chunkfile)
quad_mtime = os.stat(path[1])[stat.ST_MTIME];
quadPath_filtered.append(path)
if quad_mtime > tile_mtime:
needs_rerender = True
except OSError:
# We need to stat all the quad files, so keep looping
pass
# do they all not exist?
if quadPath_filtered == []:
if tile_mtime is not None:
os.unlink(imgpath)
return
# quit now if we don't need rerender
if not needs_rerender:
return
#logging.debug("writing out innertile {0}".format(imgpath))
# Create the actual image now
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
for path in quadPath_filtered:
try:
quad = Image.open(path[1]).resize((192,192), Image.ANTIALIAS)
img.paste(quad, path[0])
except Exception, e:
logging.warning("Couldn't open %s. It may be corrupt, you may need to delete it. %s", path[1], e)
# Save it
if self.imgformat == 'jpg':
img.save(imgpath, quality=95, subsampling=0)
else: # png
img.save(imgpath)
if self.optimizeimg:
optimize_image(imgpath, self.imgformat, self.optimizeimg)
def render_worldtile(self, chunks, colstart, colend, rowstart, rowend, path, poi_queue=None):
"""Renders just the specified chunks into a tile and save it. Unlike usual
python conventions, rowend and colend are inclusive. Additionally, the
chunks around the edges are half-way cut off (so that neighboring tiles
will render the other half)
chunks is a list of (col, row, chunkx, chunky, filename) of chunk
images that are relevant to this call (with their associated regions)
The image is saved to path+"."+self.imgformat
If there are no chunks, this tile is not saved (if it already exists, it is
deleted)
Standard tile size has colend-colstart=2 and rowend-rowstart=4
There is no return value
"""
# width of one chunk is 384. Each column is half a chunk wide. The total
# width is (384 + 192*(numcols-1)) since the first column contributes full
# width, and each additional one contributes half since they're staggered.
# However, since we want to cut off half a chunk at each end (384 less
# pixels) and since (colend - colstart + 1) is the number of columns
# inclusive, the equation simplifies to:
width = 192 * (colend - colstart)
# Same deal with height
height = 96 * (rowend - rowstart)
# The standard tile size is 3 columns by 5 rows, which works out to 384x384
# pixels for 8 total chunks. (Since the chunks are staggered but the grid
# is not, some grid coordinates do not address chunks) The two chunks on
# the middle column are shown in full, the two chunks in the middle row are
# half cut off, and the four remaining chunks are one quarter shown.
# The above example with cols 0-3 and rows 0-4 has the chunks arranged like this:
# 0,0 2,0
# 1,1
# 0,2 2,2
# 1,3
# 0,4 2,4
# Due to how the tiles fit together, we may need to render chunks way above
# this (since very few chunks actually touch the top of the sky, some tiles
# way above this one are possibly visible in this tile). Render them
# anyways just in case). "chunks" should include up to rowstart-16
imgpath = path + "." + self.imgformat
world = self.world
#stat the file, we need to know if it exists or it's mtime
try:
tile_mtime = os.stat(imgpath)[stat.ST_MTIME];
except OSError, e:
if e.errno != errno.ENOENT:
raise
tile_mtime = None
if not chunks:
# No chunks were found in this tile
if tile_mtime is not None:
os.unlink(imgpath)
return None
# Create the directory if not exists
dirdest = os.path.dirname(path)
if not os.path.exists(dirdest):
try:
os.makedirs(dirdest)
except OSError, e:
import errno
# Ignore if file doesn't exist, another task could have already
# removed it.
if e.errno != errno.ENOENT:
logging.warning("Could not remove chunk '{0}'!".format(chunkfile))
# Ignore errno EEXIST: file exists. Since this is multithreaded,
# two processes could conceivably try and create the same directory
# at the same time.
if e.errno != errno.EEXIST:
raise
else:
logging.warning("Removed the corrupt file")
# check chunk mtimes to see if they are newer
try:
needs_rerender = False
get_region_mtime = world.get_region_mtime
for col, row, chunkx, chunky, regionfile in chunks:
# check region file mtime first.
region,regionMtime = get_region_mtime(regionfile)
if regionMtime <= tile_mtime:
continue
# checking chunk mtime
if region.get_chunk_timestamp(chunkx, chunky) > tile_mtime:
needs_rerender = True
break
# if after all that, we don't need a rerender, return
if not needs_rerender:
return None
except OSError:
# couldn't get tile mtime, skip check
pass
#logging.debug("writing out worldtile {0}".format(imgpath))
logging.warning("You will need to re-run the Overviewer to fix this chunk")
continue
# Compile this image
tileimg = Image.new("RGBA", (width, height), self.bgcolor)
xpos = -192 + (col-colstart)*192
ypos = -96 + (row-rowstart)*96
world = self.world
rendermode = self.rendermode
# col colstart will get drawn on the image starting at x coordinates -(384/2)
# row rowstart will get drawn on the image starting at y coordinates -(192/2)
for col, row, chunkx, chunky, regionfile in chunks:
xpos = -192 + (col-colstart)*192
ypos = -96 + (row-rowstart)*96
composite.alpha_over(tileimg, chunkimg.convert("RGB"), (xpos, ypos), chunkimg)
# draw the chunk!
try:
a = chunk.ChunkRenderer((chunkx, chunky), world, rendermode, poi_queue)
a.chunk_render(tileimg, xpos, ypos, None)
except chunk.ChunkCorrupt:
# an error was already printed
pass
# Save them
tileimg.save(imgpath)
# Save them
tileimg.save(imgpath)
if optimizeimg:
optimize_image(imgpath, imgformat, optimizeimg)
with open(hashpath, "wb") as hashout:
hashout.write(digest)
class FakeResult(object):
def __init__(self, res):
self.res = res
def get(self):
return self.res
class FakePool(object):
"""A fake pool used to render things in sync. Implements a subset of
multiprocessing.Pool"""
def apply_async(self, func, args=(), kwargs=None):
if not kwargs:
kwargs = {}
result = func(*args, **kwargs)
return FakeResult(result)
def close(self):
pass
def join(self):
pass
if self.optimizeimg:
optimize_image(imgpath, self.imgformat, self.optimizeimg)

366
rendernode.py Normal file
View File

@@ -0,0 +1,366 @@
# 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 <http://www.gnu.org/licenses/>.
import multiprocessing
import Queue
import itertools
from itertools import cycle, islice
import os
import os.path
import functools
import re
import shutil
import collections
import json
import logging
import util
import cPickle
import stat
import errno
import time
from time import gmtime, strftime, sleep
"""
This module has routines related to distributing the render job to multipule nodes
"""
def catch_keyboardinterrupt(func):
"""Decorator that catches a keyboardinterrupt and raises a real exception
so that multiprocessing will propagate it properly"""
@functools.wraps(func)
def newfunc(*args, **kwargs):
try:
return func(*args, **kwargs)
except KeyboardInterrupt:
logging.error("Ctrl-C caught!")
raise Exception("Exiting")
except:
import traceback
traceback.print_exc()
raise
return newfunc
child_rendernode = None
def pool_initializer(rendernode):
logging.debug("Child process {0}".format(os.getpid()))
#stash the quadtree objects in a global variable after fork() for windows compat.
global child_rendernode
child_rendernode = rendernode
for quadtree in rendernode.quadtrees:
if quadtree.world.useBiomeData:
import textures
# make sure we've at least *tried* to load the color arrays in this process...
textures.prepareBiomeData(quadtree.world.worlddir)
if not textures.grasscolor or not textures.foliagecolor:
raise Exception("Can't find grasscolor.png or foliagecolor.png")
#http://docs.python.org/library/itertools.html
def roundrobin(iterables):
"roundrobin('ABC', 'D', 'EF') --> A D E B F C"
# Recipe credited to George Sakkis
pending = len(iterables)
nexts = cycle(iter(it).next for it in iterables)
while pending:
try:
for next in nexts:
yield next()
except StopIteration:
pending -= 1
nexts = cycle(islice(nexts, pending))
class RenderNode(object):
def __init__(self, quadtrees):
"""Distributes the rendering of a list of quadtrees."""
if not len(quadtrees) > 0:
raise ValueError("there must be at least one quadtree to work on")
self.quadtrees = quadtrees
#bind an index value to the quadtree so we can find it again
#and figure out which worlds are where
i = 0
self.worlds = []
for q in quadtrees:
q._render_index = i
i += 1
if q.world not in self.worlds:
self.worlds.append(q.world)
manager = multiprocessing.Manager()
# queue for receiving interesting events from the renderer
# (like the discovery of signs!
#stash into the world object like we stash an index into the quadtree
for world in self.worlds:
world.poi_q = manager.Queue()
def print_statusline(self, complete, total, level, unconditional=False):
if unconditional:
pass
elif complete < 100:
if not complete % 25 == 0:
return
elif complete < 1000:
if not complete % 100 == 0:
return
else:
if not complete % 1000 == 0:
return
logging.info("{0}/{1} tiles complete on level {2}/{3}".format(
complete, total, level, self.max_p))
def go(self, procs):
"""Renders all tiles"""
logging.debug("Parent process {0}".format(os.getpid()))
# Create a pool
if procs == 1:
pool = FakePool()
pool_initializer(self)
else:
pool = multiprocessing.Pool(processes=procs,initializer=pool_initializer,initargs=(self,))
#warm up the pool so it reports all the worker id's
if logging.getLogger().level >= 10:
pool.map(bool,xrange(multiprocessing.cpu_count()),1)
else:
pool.map_async(bool,xrange(multiprocessing.cpu_count()),1)
quadtrees = self.quadtrees
# do per-quadtree init.
max_p = 0
total = 0
for q in quadtrees:
total += 4**q.p
if q.p > max_p:
max_p = q.p
self.max_p = max_p
# Render the highest level of tiles from the chunks
results = collections.deque()
complete = 0
logging.info("Rendering highest zoom level of tiles now.")
logging.info("Rendering {0} layer{1}".format(len(quadtrees),'s' if len(quadtrees) > 1 else '' ))
logging.info("There are {0} tiles to render".format(total))
logging.info("There are {0} total levels to render".format(self.max_p))
logging.info("Don't worry, each level has only 25% as many tiles as the last.")
logging.info("The others will go faster")
count = 0
batch_size = 4*len(quadtrees)
while batch_size < 10:
batch_size *= 2
timestamp = time.time()
for result in self._apply_render_worldtiles(pool,batch_size):
results.append(result)
# every second drain some of the queue
timestamp2 = time.time()
if timestamp2 >= timestamp + 1:
timestamp = timestamp2
count_to_remove = (1000//batch_size)
if count_to_remove < len(results):
for world in self.worlds:
try:
while (1):
# an exception will break us out of this loop
item = world.poi_q.get(block=False)
if item[0] == "newpoi":
if item[1] not in world.POI:
#print "got an item from the queue!"
world.POI.append(item[1])
elif item[0] == "removePOI":
world.persistentData['POI'] = filter(lambda x: x['chunk'] != item[1], world.persistentData['POI'])
except Queue.Empty:
pass
while count_to_remove > 0:
count_to_remove -= 1
complete += results.popleft().get()
self.print_statusline(complete, total, 1)
if len(results) > (10000//batch_size):
# Empty the queue before adding any more, so that memory
# required has an upper bound
while len(results) > (500//batch_size):
complete += results.popleft().get()
self.print_statusline(complete, total, 1)
# Wait for the rest of the results
while len(results) > 0:
complete += results.popleft().get()
self.print_statusline(complete, total, 1)
for world in self.worlds:
try:
while (1):
# an exception will break us out of this loop
item = world.poi_q.get(block=False)
if item[0] == "newpoi":
if item[1] not in world.POI:
#print "got an item from the queue!"
world.POI.append(item[1])
elif item[0] == "removePOI":
world.persistentData['POI'] = filter(lambda x: x['chunk'] != item[1], world.persistentData['POI'])
except Queue.Empty:
pass
self.print_statusline(complete, total, 1, True)
# Now do the other layers
for zoom in xrange(self.max_p-1, 0, -1):
level = self.max_p - zoom + 1
assert len(results) == 0
complete = 0
total = 0
for q in quadtrees:
if zoom <= q.p:
total += 4**zoom
logging.info("Starting level {0}".format(level))
timestamp = time.time()
for result in self._apply_render_inntertile(pool, zoom,batch_size):
results.append(result)
# every second drain some of the queue
timestamp2 = time.time()
if timestamp2 >= timestamp + 1:
timestamp = timestamp2
count_to_remove = (1000//batch_size)
if count_to_remove < len(results):
while count_to_remove > 0:
count_to_remove -= 1
complete += results.popleft().get()
self.print_statusline(complete, total, level)
if len(results) > (10000/batch_size):
while len(results) > (500/batch_size):
complete += results.popleft().get()
self.print_statusline(complete, total, level)
# Empty the queue
while len(results) > 0:
complete += results.popleft().get()
self.print_statusline(complete, total, level)
self.print_statusline(complete, total, level, True)
logging.info("Done")
pool.close()
pool.join()
# Do the final one right here:
for q in quadtrees:
q.render_innertile(os.path.join(q.destdir, q.tiledir), "base")
def _apply_render_worldtiles(self, pool,batch_size):
"""Returns an iterator over result objects. Each time a new result is
requested, a new task is added to the pool and a result returned.
"""
if batch_size < len(self.quadtrees):
batch_size = len(self.quadtrees)
batch = []
jobcount = 0
# roundrobin add tiles to a batch job (thus they should all roughly work on similar chunks)
iterables = [q.get_worldtiles() for q in self.quadtrees]
for job in roundrobin(iterables):
# fixup so the worker knows which quadtree this is
job[0] = job[0]._render_index
# Put this in the batch to be submited to the pool
batch.append(job)
jobcount += 1
if jobcount >= batch_size:
jobcount = 0
yield pool.apply_async(func=render_worldtile_batch, args= [batch])
batch = []
if jobcount > 0:
yield pool.apply_async(func=render_worldtile_batch, args= [batch])
def _apply_render_inntertile(self, pool, zoom,batch_size):
"""Same as _apply_render_worltiles but for the inntertile routine.
Returns an iterator that yields result objects from tasks that have
been applied to the pool.
"""
if batch_size < len(self.quadtrees):
batch_size = len(self.quadtrees)
batch = []
jobcount = 0
# roundrobin add tiles to a batch job (thus they should all roughly work on similar chunks)
iterables = [q.get_innertiles(zoom) for q in self.quadtrees if zoom <= q.p]
for job in roundrobin(iterables):
# fixup so the worker knows which quadtree this is
job[0] = job[0]._render_index
# Put this in the batch to be submited to the pool
batch.append(job)
jobcount += 1
if jobcount >= batch_size:
jobcount = 0
yield pool.apply_async(func=render_innertile_batch, args= [batch])
batch = []
if jobcount > 0:
yield pool.apply_async(func=render_innertile_batch, args= [batch])
@catch_keyboardinterrupt
def render_worldtile_batch(batch):
global child_rendernode
rendernode = child_rendernode
count = 0
#logging.debug("{0} working on batch of size {1}".format(os.getpid(),len(batch)))
for job in batch:
count += 1
quadtree = rendernode.quadtrees[job[0]]
colstart = job[1]
colend = job[2]
rowstart = job[3]
rowend = job[4]
path = job[5]
poi_queue = quadtree.world.poi_q
path = quadtree.full_tiledir+os.sep+path
# (even if tilechunks is empty, render_worldtile will delete
# existing images if appropriate)
# And uses these chunks
tilechunks = quadtree.get_chunks_in_range(colstart, colend, rowstart,rowend)
#logging.debug(" tilechunks: %r", tilechunks)
quadtree.render_worldtile(tilechunks,colstart, colend, rowstart, rowend, path, poi_queue)
return count
@catch_keyboardinterrupt
def render_innertile_batch(batch):
global child_rendernode
rendernode = child_rendernode
count = 0
#logging.debug("{0} working on batch of size {1}".format(os.getpid(),len(batch)))
for job in batch:
count += 1
quadtree = rendernode.quadtrees[job[0]]
dest = quadtree.full_tiledir+os.sep+job[1]
quadtree.render_innertile(dest=dest,name=job[2])
return count
class FakeResult(object):
def __init__(self, res):
self.res = res
def get(self):
return self.res
class FakePool(object):
"""A fake pool used to render things in sync. Implements a subset of
multiprocessing.Pool"""
def apply_async(self, func, args=(), kwargs=None):
if not kwargs:
kwargs = {}
result = func(*args, **kwargs)
return FakeResult(result)
def close(self):
pass
def join(self):
pass

149
sample.settings.py Normal file
View File

@@ -0,0 +1,149 @@
################################################################################
# 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 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
# Lines that start with a hash mark are comments
# Some variables come with defaults (like procs or rendermode)
# If you specify a configuration option in both a settings.py file and on the
# command line, the value from the command line will take precedence
################################################################################
### procs
## Specify the number of processors to use for rendering
## Default: The number of CPU cores on your machine
## Type: integer
## Example: set the number of processors to use to be 1 less than the number of
## CPU cpus in your machine
import multiprocessing
procs = multiprocessing.cpu_count() - 1
if procs < 1: procs = 1
################################################################################
### zoom
## Sets the zoom level manually instead of calculating it. This can be useful
## if you have outlier chunks that make your world too big. This value will
## make the highest zoom level contain (2**ZOOM)^2 tiles
## Normally you should not need to set this variable.
## Default: Automatically calculated from your world
## Type: integer
## Example:
zoom = 9
################################################################################
### regionlist
## 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.
## Default: not yet
## Type: string
## Example: Dynamically create regionlist of only regions older than 2 days
import os, time
regionDir = os.path.join(args[0], "region")
regionFiles = filter(lambda x: x.endswith(".mcr"), os.listdir(regionDir))
def olderThanTwoDays(f):
return time.time() - os.stat(f).st_mtime > (60*60*24*2)
oldRegionFiles = filter(olderThanTwoDays, regionFiles)
with open("regionlist.txt", "w") as f:
f.write("\n".join(oldRegionFiles))
################################################################################
### rendermode
## Specifies the render types
## Default: "normal"
## Type: Either a list of strings, or a single string containing modes separated
## by commas
## Example: Render the using the 'lighting' mode, but if today is Sunday, then
## also render the 'night' mode
import time
rendermode=["lighting"]
if time.localtime().tm_wday == 6:
rendermode.append("night")
################################################################################
### imgformat
## The image output format to use. Currently supported: png(default), jpg.
## NOTE: png will always be used as the intermediate image format.
## Default: not yet
## Type: string
## Example:
imgformat = "jpg"
################################################################################
### optimizeimg
## 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%
## Default: not set
## Type: integer
## Example:
if imgformat != "jpg":
optimizeimg = 2
################################################################################
### web_assets_hook
## If provided, run this function after the web assets have been copied, but
## before actual tile rendering beings. It should accept a QuadtreeGen
## object as its only argument. Note: this is only called if skipjs is True
## Default: not yet
## Type: function
## Example: Call an external program to generate something useful
def web_assets_hook(o):
import subprocess
p = subprocess.Popen(["/path/to/my/script.pl", "--output_dir", args[1]])
p.wait()
if p.returncode != 0:
raise Exception("web_assets_hook failed")
################################################################################
### quiet
## Print less output. You can specify higher values to suppress additional output
## Default: 0
## Type: integer
## Example:
quiet = 1
################################################################################
### verbose
## Print more output. You can specify higher values to print additional output
## Default: 0
## Type: integer
## Example:
verbose = 1
################################################################################
### skipjs
## Don't output marker.js or region.js
## Default: False
## Type: boolean
## Example: Set skipjs if web_assets_hook is defined
if "web_assets_hook" in locals():
skipjs = True

View File

@@ -1,11 +1,13 @@
from distutils.core import setup, Extension
from distutils.command.build import build
from distutils.command.clean import clean
from distutils.command.build_ext import build_ext
from distutils.dir_util import remove_tree
from distutils import log
import os, os.path
import glob
import platform
import time
try:
import py2exe
@@ -23,24 +25,45 @@ setup_kwargs['cmdclass'] = {}
# py2exe options
#
if py2exe != None:
setup_kwargs['console'] = ['gmap.py']
if py2exe is not None:
setup_kwargs['console'] = ['overviewer.py']
setup_kwargs['data_files'] = [('textures', ['textures/lava.png', 'textures/water.png', 'textures/fire.png']),
('', ['config.js', 'COPYING.txt', 'README.rst']),
('', ['overviewerConfig.js', 'COPYING.txt', 'README.rst']),
('web_assets', glob.glob('web_assets/*'))]
setup_kwargs['zipfile'] = None
setup_kwargs['options']['py2exe'] = {'bundle_files' : 1, 'excludes': 'Tkinter'}
if platform.system() == 'Windows' and '64bit' in platform.architecture():
b = 3
else:
b = 1
setup_kwargs['options']['py2exe'] = {'bundle_files' : b, 'excludes': 'Tkinter'}
#
# _composite.c extension
# c_overviewer extension
#
# Third-party modules - we depend on numpy for everything
import numpy
# Obtain the numpy include directory. This logic works across numpy versions.
try:
numpy_include = numpy.get_include()
except AttributeError:
numpy_include = numpy.get_numpy_include()
try:
pil_include = os.environ['PIL_INCLUDE_DIR'].split(os.pathsep)
except:
pil_include = []
setup_kwargs['ext_modules'].append(Extension('_composite', ['_composite.c'], include_dirs=['.'] + pil_include, extra_link_args=["/MANIFEST"] if platform.system() == "Windows" else []))
# used to figure out what files to compile
render_modes = ['normal', 'overlay', 'lighting', 'night', 'spawn', 'cave']
c_overviewer_files = ['src/main.c', 'src/composite.c', 'src/iterate.c', 'src/endian.c', 'src/rendermodes.c']
c_overviewer_files += map(lambda mode: 'src/rendermode-%s.c' % (mode,), render_modes)
c_overviewer_files += ['src/Draw.c']
c_overviewer_includes = ['src/overviewer.h', 'src/rendermodes.h']
setup_kwargs['ext_modules'].append(Extension('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}
@@ -56,7 +79,7 @@ class CustomClean(clean):
# try to remove '_composite.{so,pyd,...}' extension,
# regardless of the current system's extension name convention
build_ext = self.get_finalized_command('build_ext')
pretty_fname = build_ext.get_ext_filename('_composite')
pretty_fname = build_ext.get_ext_filename('c_overviewer')
fname = pretty_fname
if os.path.exists(fname):
try:
@@ -69,8 +92,41 @@ class CustomClean(clean):
else:
log.debug("'%s' does not exist -- can't clean it",
pretty_fname)
setup_kwargs['cmdclass']['clean'] = CustomClean
class CustomBuild(build_ext):
def build_extensions(self):
c = self.compiler.compiler_type
if c == "msvc":
# customize the build options for this compilier
for e in self.extensions:
e.extra_link_args.append("/MANIFEST")
build_ext.build_extensions(self)
if py2exe is not None:
# define a subclass of py2exe to build our version file on the fly
class CustomPy2exe(py2exe.build_exe.py2exe):
def run(self):
try:
import util
f = open("overviewer_version.py", "w")
f.write("VERSION=%r\n" % util.findGitVersion())
f.write("BUILD_DATE=%r\n" % time.asctime())
f.write("BUILD_PLATFORM=%r\n" % platform.processor())
f.write("BUILD_OS=%r\n" % platform.platform())
f.close()
setup_kwargs['data_files'].append(('.', ['overviewer_version.py']))
except:
print "WARNING: failed to build overview_version file"
py2exe.build_exe.py2exe.run(self)
setup_kwargs['cmdclass']['py2exe'] = CustomPy2exe
setup_kwargs['cmdclass']['clean'] = CustomClean
setup_kwargs['cmdclass']['build_ext'] = CustomBuild
###
setup(**setup_kwargs)
print "\nBuild Complete"

902
src/Draw.c Normal file
View File

@@ -0,0 +1,902 @@
/*
* The Python Imaging Library.
* $Id$
*
* a simple drawing package for the Imaging library
*
* history:
* 1996-04-13 fl Created.
* 1996-04-30 fl Added transforms and polygon support.
* 1996-08-12 fl Added filled polygons.
* 1996-11-05 fl Fixed float/int confusion in polygon filler
* 1997-07-04 fl Support 32-bit images (C++ would have been nice)
* 1998-09-09 fl Eliminated qsort casts; improved rectangle clipping
* 1998-09-10 fl Fixed fill rectangle to include lower edge (!)
* 1998-12-29 fl Added arc, chord, and pieslice primitives
* 1999-01-10 fl Added some level 2 ("arrow") stuff (experimental)
* 1999-02-06 fl Added bitmap primitive
* 1999-07-26 fl Eliminated a compiler warning
* 1999-07-31 fl Pass ink as void* instead of int
* 2002-12-10 fl Added experimental RGBA-on-RGB drawing
* 2004-09-04 fl Support simple wide lines (no joins)
* 2005-05-25 fl Fixed line width calculation
* 2011-04-01 Modified for use in Minecraft-Overviewer
*
* Copyright (c) 1996-2006 by Fredrik Lundh
* Copyright (c) 1997-2006 by Secret Labs AB.
*
* This file is part of the Python Imaging Library
*
* By obtaining, using, and/or copying this software and/or its associated
* documentation, you agree that you have read, understood, and will comply
* with the following terms and conditions:
*
* Permission to use, copy, modify, and distribute this software and its
* associated documentation for any purpose and without fee is hereby granted,
* provided that the above copyright notice appears in all copies, and that
* both that copyright notice and this permission notice appear in supporting
* documentation, and that the name of Secret Labs AB or the author not be used
* in advertising or publicity pertaining to distribution of the software
* without specific, written prior permission.
*
* SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS
* SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS.
* IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR BE LIABLE FOR ANY SPECIAL,
* INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
* LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
* OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
* PERFORMANCE OF THIS SOFTWARE.
*
*/
/* FIXME: support fill/outline attribute for all filled shapes */
/* FIXME: support zero-winding fill */
/* FIXME: add drawing context, support affine transforms */
/* FIXME: support clip window (and mask?) */
#include "Imaging.h"
#include <math.h>
#define CEIL(v) (int) ceil(v)
#define FLOOR(v) ((v) >= 0.0 ? (int) (v) : (int) floor(v))
#define INK8(ink) (*(UINT8*)ink)
#define INK32(ink) (*(INT32*)ink)
/* like (a * b + 127) / 255), but much faster on most platforms */
#define MULDIV255(a, b, tmp)\
(tmp = (a) * (b) + 128, ((((tmp) >> 8) + (tmp)) >> 8))
#define BLEND(mask, in1, in2, tmp1, tmp2)\
(MULDIV255(in1, 255 - mask, tmp1) + MULDIV255(in2, mask, tmp2))
/* -------------------------------------------------------------------- */
/* Primitives */
/* -------------------------------------------------------------------- */
typedef struct {
/* edge descriptor for polygon engine */
int d;
int x0, y0;
int xmin, ymin, xmax, ymax;
float dx;
} Edge;
static inline void
point8(Imaging im, int x, int y, int ink)
{
if (x >= 0 && x < im->xsize && y >= 0 && y < im->ysize)
im->image8[y][x] = (UINT8) ink;
}
static inline void
point32(Imaging im, int x, int y, int ink)
{
if (x >= 0 && x < im->xsize && y >= 0 && y < im->ysize)
im->image32[y][x] = ink;
}
static inline void
point32rgba(Imaging im, int x, int y, int ink)
{
unsigned int tmp1, tmp2;
if (x >= 0 && x < im->xsize && y >= 0 && y < im->ysize) {
UINT8* out = (UINT8*) im->image[y]+x*4;
UINT8* in = (UINT8*) &ink;
out[0] = BLEND(in[3], out[0], in[0], tmp1, tmp2);
out[1] = BLEND(in[3], out[1], in[1], tmp1, tmp2);
out[2] = BLEND(in[3], out[2], in[2], tmp1, tmp2);
}
}
static inline void
hline8(Imaging im, int x0, int y0, int x1, int ink)
{
int tmp;
if (y0 >= 0 && y0 < im->ysize) {
if (x0 > x1)
tmp = x0, x0 = x1, x1 = tmp;
if (x0 < 0)
x0 = 0;
else if (x0 >= im->xsize)
return;
if (x1 < 0)
return;
else if (x1 >= im->xsize)
x1 = im->xsize-1;
if (x0 <= x1)
memset(im->image8[y0] + x0, (UINT8) ink, x1 - x0 + 1);
}
}
static inline void
hline32(Imaging im, int x0, int y0, int x1, int ink)
{
int tmp;
INT32* p;
if (y0 >= 0 && y0 < im->ysize) {
if (x0 > x1)
tmp = x0, x0 = x1, x1 = tmp;
if (x0 < 0)
x0 = 0;
else if (x0 >= im->xsize)
return;
if (x1 < 0)
return;
else if (x1 >= im->xsize)
x1 = im->xsize-1;
p = im->image32[y0];
while (x0 <= x1)
p[x0++] = ink;
}
}
static inline void
hline32rgba(Imaging im, int x0, int y0, int x1, int ink)
{
int tmp;
unsigned int tmp1, tmp2;
if (y0 >= 0 && y0 < im->ysize) {
if (x0 > x1)
tmp = x0, x0 = x1, x1 = tmp;
if (x0 < 0)
x0 = 0;
else if (x0 >= im->xsize)
return;
if (x1 < 0)
return;
else if (x1 >= im->xsize)
x1 = im->xsize-1;
if (x0 <= x1) {
UINT8* out = (UINT8*) im->image[y0]+x0*4;
UINT8* in = (UINT8*) &ink;
while (x0 <= x1) {
out[0] = BLEND(in[3], out[0], in[0], tmp1, tmp2);
out[1] = BLEND(in[3], out[1], in[1], tmp1, tmp2);
out[2] = BLEND(in[3], out[2], in[2], tmp1, tmp2);
x0++; out += 4;
}
}
}
}
static inline void
line8(Imaging im, int x0, int y0, int x1, int y1, int ink)
{
int i, n, e;
int dx, dy;
int xs, ys;
/* normalize coordinates */
dx = x1-x0;
if (dx < 0)
dx = -dx, xs = -1;
else
xs = 1;
dy = y1-y0;
if (dy < 0)
dy = -dy, ys = -1;
else
ys = 1;
n = (dx > dy) ? dx : dy;
if (dx == 0)
/* vertical */
for (i = 0; i < dy; i++) {
point8(im, x0, y0, ink);
y0 += ys;
}
else if (dy == 0)
/* horizontal */
for (i = 0; i < dx; i++) {
point8(im, x0, y0, ink);
x0 += xs;
}
else if (dx > dy) {
/* bresenham, horizontal slope */
n = dx;
dy += dy;
e = dy - dx;
dx += dx;
for (i = 0; i < n; i++) {
point8(im, x0, y0, ink);
if (e >= 0) {
y0 += ys;
e -= dx;
}
e += dy;
x0 += xs;
}
} else {
/* bresenham, vertical slope */
n = dy;
dx += dx;
e = dx - dy;
dy += dy;
for (i = 0; i < n; i++) {
point8(im, x0, y0, ink);
if (e >= 0) {
x0 += xs;
e -= dy;
}
e += dx;
y0 += ys;
}
}
}
static inline void
line32(Imaging im, int x0, int y0, int x1, int y1, int ink)
{
int i, n, e;
int dx, dy;
int xs, ys;
/* normalize coordinates */
dx = x1-x0;
if (dx < 0)
dx = -dx, xs = -1;
else
xs = 1;
dy = y1-y0;
if (dy < 0)
dy = -dy, ys = -1;
else
ys = 1;
n = (dx > dy) ? dx : dy;
if (dx == 0)
/* vertical */
for (i = 0; i < dy; i++) {
point32(im, x0, y0, ink);
y0 += ys;
}
else if (dy == 0)
/* horizontal */
for (i = 0; i < dx; i++) {
point32(im, x0, y0, ink);
x0 += xs;
}
else if (dx > dy) {
/* bresenham, horizontal slope */
n = dx;
dy += dy;
e = dy - dx;
dx += dx;
for (i = 0; i < n; i++) {
point32(im, x0, y0, ink);
if (e >= 0) {
y0 += ys;
e -= dx;
}
e += dy;
x0 += xs;
}
} else {
/* bresenham, vertical slope */
n = dy;
dx += dx;
e = dx - dy;
dy += dy;
for (i = 0; i < n; i++) {
point32(im, x0, y0, ink);
if (e >= 0) {
x0 += xs;
e -= dy;
}
e += dx;
y0 += ys;
}
}
}
static inline void
line32rgba(Imaging im, int x0, int y0, int x1, int y1, int ink)
{
int i, n, e;
int dx, dy;
int xs, ys;
/* normalize coordinates */
dx = x1-x0;
if (dx < 0)
dx = -dx, xs = -1;
else
xs = 1;
dy = y1-y0;
if (dy < 0)
dy = -dy, ys = -1;
else
ys = 1;
n = (dx > dy) ? dx : dy;
if (dx == 0)
/* vertical */
for (i = 0; i < dy; i++) {
point32rgba(im, x0, y0, ink);
y0 += ys;
}
else if (dy == 0)
/* horizontal */
for (i = 0; i < dx; i++) {
point32rgba(im, x0, y0, ink);
x0 += xs;
}
else if (dx > dy) {
/* bresenham, horizontal slope */
n = dx;
dy += dy;
e = dy - dx;
dx += dx;
for (i = 0; i < n; i++) {
point32rgba(im, x0, y0, ink);
if (e >= 0) {
y0 += ys;
e -= dx;
}
e += dy;
x0 += xs;
}
} else {
/* bresenham, vertical slope */
n = dy;
dx += dx;
e = dx - dy;
dy += dy;
for (i = 0; i < n; i++) {
point32rgba(im, x0, y0, ink);
if (e >= 0) {
x0 += xs;
e -= dy;
}
e += dx;
y0 += ys;
}
}
}
static int
x_cmp(const void *x0, const void *x1)
{
float diff = *((float*)x0) - *((float*)x1);
if (diff < 0)
return -1;
else if (diff > 0)
return 1;
else
return 0;
}
static inline int
polygon8(Imaging im, int n, Edge *e, int ink, int eofill)
{
int i, j;
float *xx;
int ymin, ymax;
float y;
if (n <= 0)
return 0;
/* Find upper and lower polygon boundary (within image) */
ymin = e[0].ymin;
ymax = e[0].ymax;
for (i = 1; i < n; i++) {
if (e[i].ymin < ymin) ymin = e[i].ymin;
if (e[i].ymax > ymax) ymax = e[i].ymax;
}
if (ymin < 0)
ymin = 0;
if (ymax >= im->ysize)
ymax = im->ysize-1;
/* Process polygon edges */
xx = malloc(n * sizeof(float));
if (!xx)
return -1;
for (;ymin <= ymax; ymin++) {
y = ymin+0.5F;
for (i = j = 0; i < n; i++)
if (y >= e[i].ymin && y <= e[i].ymax) {
if (e[i].d == 0)
hline8(im, e[i].xmin, ymin, e[i].xmax, ink);
else
xx[j++] = (y-e[i].y0) * e[i].dx + e[i].x0;
}
if (j == 2) {
if (xx[0] < xx[1])
hline8(im, CEIL(xx[0]-0.5), ymin, FLOOR(xx[1]+0.5), ink);
else
hline8(im, CEIL(xx[1]-0.5), ymin, FLOOR(xx[0]+0.5), ink);
} else {
qsort(xx, j, sizeof(float), x_cmp);
for (i = 0; i < j-1 ; i += 2)
hline8(im, CEIL(xx[i]-0.5), ymin, FLOOR(xx[i+1]+0.5), ink);
}
}
free(xx);
return 0;
}
static inline int
polygon32(Imaging im, int n, Edge *e, int ink, int eofill)
{
int i, j;
float *xx;
int ymin, ymax;
float y;
if (n <= 0)
return 0;
/* Find upper and lower polygon boundary (within image) */
ymin = e[0].ymin;
ymax = e[0].ymax;
for (i = 1; i < n; i++) {
if (e[i].ymin < ymin) ymin = e[i].ymin;
if (e[i].ymax > ymax) ymax = e[i].ymax;
}
if (ymin < 0)
ymin = 0;
if (ymax >= im->ysize)
ymax = im->ysize-1;
/* Process polygon edges */
xx = malloc(n * sizeof(float));
if (!xx)
return -1;
for (;ymin <= ymax; ymin++) {
y = ymin+0.5F;
for (i = j = 0; i < n; i++) {
if (y >= e[i].ymin && y <= e[i].ymax) {
if (e[i].d == 0)
hline32(im, e[i].xmin, ymin, e[i].xmax, ink);
else
xx[j++] = (y-e[i].y0) * e[i].dx + e[i].x0;
}
}
if (j == 2) {
if (xx[0] < xx[1])
hline32(im, CEIL(xx[0]-0.5), ymin, FLOOR(xx[1]+0.5), ink);
else
hline32(im, CEIL(xx[1]-0.5), ymin, FLOOR(xx[0]+0.5), ink);
} else {
qsort(xx, j, sizeof(float), x_cmp);
for (i = 0; i < j-1 ; i += 2)
hline32(im, CEIL(xx[i]-0.5), ymin, FLOOR(xx[i+1]+0.5), ink);
}
}
free(xx);
return 0;
}
static inline int
polygon32rgba(Imaging im, int n, Edge *e, int ink, int eofill)
{
int i, j;
float *xx;
int ymin, ymax;
float y;
if (n <= 0)
return 0;
/* Find upper and lower polygon boundary (within image) */
ymin = e[0].ymin;
ymax = e[0].ymax;
for (i = 1; i < n; i++) {
if (e[i].ymin < ymin) ymin = e[i].ymin;
if (e[i].ymax > ymax) ymax = e[i].ymax;
}
if (ymin < 0)
ymin = 0;
if (ymax >= im->ysize)
ymax = im->ysize-1;
/* Process polygon edges */
xx = malloc(n * sizeof(float));
if (!xx)
return -1;
for (;ymin <= ymax; ymin++) {
y = ymin+0.5F;
for (i = j = 0; i < n; i++) {
if (y >= e[i].ymin && y <= e[i].ymax) {
if (e[i].d == 0)
hline32rgba(im, e[i].xmin, ymin, e[i].xmax, ink);
else
xx[j++] = (y-e[i].y0) * e[i].dx + e[i].x0;
}
}
if (j == 2) {
if (xx[0] < xx[1])
hline32rgba(im, CEIL(xx[0]-0.5), ymin, FLOOR(xx[1]+0.5), ink);
else
hline32rgba(im, CEIL(xx[1]-0.5), ymin, FLOOR(xx[0]+0.5), ink);
} else {
qsort(xx, j, sizeof(float), x_cmp);
for (i = 0; i < j-1 ; i += 2)
hline32rgba(im, CEIL(xx[i]-0.5), ymin, FLOOR(xx[i+1]+0.5), ink);
}
}
free(xx);
return 0;
}
static inline void
add_edge(Edge *e, int x0, int y0, int x1, int y1)
{
/* printf("edge %d %d %d %d\n", x0, y0, x1, y1); */
if (x0 <= x1)
e->xmin = x0, e->xmax = x1;
else
e->xmin = x1, e->xmax = x0;
if (y0 <= y1)
e->ymin = y0, e->ymax = y1;
else
e->ymin = y1, e->ymax = y0;
if (y0 == y1) {
e->d = 0;
e->dx = 0.0;
} else {
e->dx = ((float)(x1-x0)) / (y1-y0);
if (y0 == e->ymin)
e->d = 1;
else
e->d = -1;
}
e->x0 = x0;
e->y0 = y0;
}
typedef struct {
void (*point)(Imaging im, int x, int y, int ink);
void (*hline)(Imaging im, int x0, int y0, int x1, int ink);
void (*line)(Imaging im, int x0, int y0, int x1, int y1, int ink);
int (*polygon)(Imaging im, int n, Edge *e, int ink, int eofill);
} DRAW;
DRAW draw8 = { point8, hline8, line8, polygon8 };
DRAW draw32 = { point32, hline32, line32, polygon32 };
DRAW draw32rgba = { point32rgba, hline32rgba, line32rgba, polygon32rgba };
/* -------------------------------------------------------------------- */
/* Interface */
/* -------------------------------------------------------------------- */
#define DRAWINIT()\
if (im->image8) {\
draw = &draw8;\
ink = INK8(ink_);\
} else {\
draw = (op) ? &draw32rgba : &draw32; \
ink = INK32(ink_);\
}
int
ImagingDrawPoint(Imaging im, int x0, int y0, const void* ink_, int op)
{
DRAW* draw;
INT32 ink;
DRAWINIT();
draw->point(im, x0, y0, ink);
return 0;
}
int
ImagingDrawLine(Imaging im, int x0, int y0, int x1, int y1, const void* ink_,
int op)
{
DRAW* draw;
INT32 ink;
DRAWINIT();
draw->line(im, x0, y0, x1, y1, ink);
return 0;
}
int
ImagingDrawWideLine(Imaging im, int x0, int y0, int x1, int y1,
const void* ink_, int width, int op)
{
DRAW* draw;
INT32 ink;
Edge e[4];
int dx, dy;
double d;
DRAWINIT();
if (width <= 1) {
draw->line(im, x0, y0, x1, y1, ink);
return 0;
}
dx = x1-x0;
dy = y1-y0;
if (dx == 0 && dy == 0) {
draw->point(im, x0, y0, ink);
return 0;
}
d = width / sqrt((float) (dx*dx + dy*dy)) / 2.0;
dx = (int) floor(d * (y1-y0) + 0.5);
dy = (int) floor(d * (x1-x0) + 0.5);
add_edge(e+0, x0 - dx, y0 + dy, x1 - dx, y1 + dy);
add_edge(e+1, x1 - dx, y1 + dy, x1 + dx, y1 - dy);
add_edge(e+2, x1 + dx, y1 - dy, x0 + dx, y0 - dy);
add_edge(e+3, x0 + dx, y0 - dy, x0 - dx, y0 + dy);
draw->polygon(im, 4, e, ink, 0);
return 0;
}
/* -------------------------------------------------------------------- */
/* standard shapes */
#define ARC 0
#define CHORD 1
#define PIESLICE 2
/* -------------------------------------------------------------------- */
/* experimental level 2 ("arrow") graphics stuff. this implements
portions of the arrow api on top of the Edge structure. the
semantics are ok, except that "curve" flattens the bezier curves by
itself */
#if 1 /* ARROW_GRAPHICS */
struct ImagingOutlineInstance {
float x0, y0;
float x, y;
int count;
Edge *edges;
int size;
};
void
ImagingOutlineDelete(ImagingOutline outline)
{
if (!outline)
return;
if (outline->edges)
free(outline->edges);
free(outline);
}
static Edge*
allocate(ImagingOutline outline, int extra)
{
Edge* e;
if (outline->count + extra > outline->size) {
/* expand outline buffer */
outline->size += extra + 25;
if (!outline->edges)
e = malloc(outline->size * sizeof(Edge));
else
e = realloc(outline->edges, outline->size * sizeof(Edge));
if (!e)
return NULL;
outline->edges = e;
}
e = outline->edges + outline->count;
outline->count += extra;
return e;
}
int
ImagingOutlineMove(ImagingOutline outline, float x0, float y0)
{
outline->x = outline->x0 = x0;
outline->y = outline->y0 = y0;
return 0;
}
int
ImagingOutlineLine(ImagingOutline outline, float x1, float y1)
{
Edge* e;
e = allocate(outline, 1);
if (!e)
return -1; /* out of memory */
add_edge(e, (int) outline->x, (int) outline->y, (int) x1, (int) y1);
outline->x = x1;
outline->y = y1;
return 0;
}
int
ImagingOutlineCurve(ImagingOutline outline, float x1, float y1,
float x2, float y2, float x3, float y3)
{
Edge* e;
int i;
float xo, yo;
#define STEPS 32
e = allocate(outline, STEPS);
if (!e)
return -1; /* out of memory */
xo = outline->x;
yo = outline->y;
/* flatten the bezier segment */
for (i = 1; i <= STEPS; i++) {
float t = ((float) i) / STEPS;
float t2 = t*t;
float t3 = t2*t;
float u = 1.0F - t;
float u2 = u*u;
float u3 = u2*u;
float x = outline->x*u3 + 3*(x1*t*u2 + x2*t2*u) + x3*t3 + 0.5;
float y = outline->y*u3 + 3*(y1*t*u2 + y2*t2*u) + y3*t3 + 0.5;
add_edge(e++, xo, yo, (int) x, (int) y);
xo = x, yo = y;
}
outline->x = xo;
outline->y = yo;
return 0;
}
int
ImagingOutlineCurve2(ImagingOutline outline, float cx, float cy,
float x3, float y3)
{
/* add bezier curve based on three control points (as
in the Flash file format) */
return ImagingOutlineCurve(
outline,
(outline->x + cx + cx)/3, (outline->y + cy + cy)/3,
(cx + cx + x3)/3, (cy + cy + y3)/3,
x3, y3);
}
int
ImagingOutlineClose(ImagingOutline outline)
{
if (outline->x == outline->x0 && outline->y == outline->y0)
return 0;
return ImagingOutlineLine(outline, outline->x0, outline->y0);
}
int
ImagingDrawOutline(Imaging im, ImagingOutline outline, const void* ink_,
int fill, int op)
{
DRAW* draw;
INT32 ink;
DRAWINIT();
draw->polygon(im, outline->count, outline->edges, ink, 0);
return 0;
}
#endif

360
src/composite.c Normal file
View File

@@ -0,0 +1,360 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
/*
* This file implements a custom alpha_over function for (some) PIL
* images. It's designed to be used through composite.py, which
* includes a proxy alpha_over function that falls back to the default
* PIL paste if this extension is not found.
*/
#include "overviewer.h"
/* like (a * b + 127) / 255), but much faster on most platforms
from PIL's _imaging.c */
#define MULDIV255(a, b, tmp) \
(tmp = (a) * (b) + 128, ((((tmp) >> 8) + (tmp)) >> 8))
typedef struct {
PyObject_HEAD
Imaging image;
} ImagingObject;
inline Imaging
imaging_python_to_c(PyObject *obj)
{
PyObject *im;
Imaging image;
/* first, get the 'im' attribute */
im = PyObject_GetAttrString(obj, "im");
if (!im)
return NULL;
/* make sure 'im' is the right type */
if (strcmp(im->ob_type->tp_name, "ImagingCore") != 0) {
/* it's not -- raise an error and exit */
PyErr_SetString(PyExc_TypeError,
"image attribute 'im' is not a core Imaging type");
return NULL;
}
image = ((ImagingObject *)im)->image;
Py_DECREF(im);
return image;
}
/* helper function to setup s{x,y}, d{x,y}, and {x,y}size variables
in these composite functions -- even handles auto-sizing to src! */
static inline void
setup_source_destination(Imaging src, Imaging dest,
int *sx, int *sy, int *dx, int *dy, int *xsize, int *ysize)
{
/* handle negative/zero sizes appropriately */
if (*xsize <= 0 || *ysize <= 0) {
*xsize = src->xsize;
*ysize = src->ysize;
}
/* set up the source position, size and destination position */
/* handle negative dest pos */
if (*dx < 0) {
*sx = -(*dx);
*dx = 0;
} else {
*sx = 0;
}
if (*dy < 0) {
*sy = -(*dy);
*dy = 0;
} else {
*sy = 0;
}
/* set up source dimensions */
*xsize -= *sx;
*ysize -= *sy;
/* clip dimensions, if needed */
if (*dx + *xsize > dest->xsize)
*xsize = dest->xsize - *dx;
if (*dy + *ysize > dest->ysize)
*ysize = dest->ysize - *dy;
}
/* convenience alpha_over with 1.0 as overall_alpha */
inline PyObject* alpha_over(PyObject *dest, PyObject *src, PyObject *mask,
int dx, int dy, int xsize, int ysize) {
return alpha_over_full(dest, src, mask, 1.0f, dx, dy, xsize, ysize);
}
/* the full alpha_over function, in a form that can be called from C
* overall_alpha is multiplied with the whole mask, useful for lighting...
* if xsize, ysize are negative, they are instead set to the size of the image in src
* returns NULL on error, dest on success. You do NOT need to decref the return!
*/
inline PyObject *
alpha_over_full(PyObject *dest, PyObject *src, PyObject *mask, float overall_alpha,
int dx, int dy, int xsize, int ysize) {
/* libImaging handles */
Imaging imDest, imSrc, imMask;
/* cached blend properties */
int src_has_alpha, mask_offset, mask_stride;
/* source position */
int sx, sy;
/* iteration variables */
unsigned int x, y, i;
/* temporary calculation variables */
int tmp1, tmp2, tmp3;
/* integer [0, 255] version of overall_alpha */
UINT8 overall_alpha_int = 255 * overall_alpha;
/* short-circuit this whole thing if overall_alpha is zero */
if (overall_alpha_int == 0)
return dest;
imDest = imaging_python_to_c(dest);
imSrc = imaging_python_to_c(src);
imMask = imaging_python_to_c(mask);
if (!imDest || !imSrc || !imMask)
return NULL;
/* check the various image modes, make sure they make sense */
if (strcmp(imDest->mode, "RGBA") != 0) {
PyErr_SetString(PyExc_ValueError,
"given destination image does not have mode \"RGBA\"");
return NULL;
}
if (strcmp(imSrc->mode, "RGBA") != 0 && strcmp(imSrc->mode, "RGB") != 0) {
PyErr_SetString(PyExc_ValueError,
"given source image does not have mode \"RGBA\" or \"RGB\"");
return NULL;
}
if (strcmp(imMask->mode, "RGBA") != 0 && strcmp(imMask->mode, "L") != 0) {
PyErr_SetString(PyExc_ValueError,
"given mask image does not have mode \"RGBA\" or \"L\"");
return NULL;
}
/* make sure mask size matches src size */
if (imSrc->xsize != imMask->xsize || imSrc->ysize != imMask->ysize) {
PyErr_SetString(PyExc_ValueError,
"mask and source image sizes do not match");
return NULL;
}
/* set up flags for the src/mask type */
src_has_alpha = (imSrc->pixelsize == 4 ? 1 : 0);
/* how far into image the first alpha byte resides */
mask_offset = (imMask->pixelsize == 4 ? 3 : 0);
/* how many bytes to skip to get to the next alpha byte */
mask_stride = imMask->pixelsize;
/* setup source & destination vars */
setup_source_destination(imSrc, imDest, &sx, &sy, &dx, &dy, &xsize, &ysize);
/* check that there remains any blending to be done */
if (xsize <= 0 || ysize <= 0) {
/* nothing to do, return */
return dest;
}
for (y = 0; y < ysize; y++) {
UINT8 *out = (UINT8 *)imDest->image[dy + y] + dx * 4;
UINT8 *outmask = (UINT8 *)imDest->image[dy + y] + dx * 4 + 3;
UINT8 *in = (UINT8 *)imSrc->image[sy + y] + sx * (imSrc->pixelsize);
UINT8 *inmask = (UINT8 *)imMask->image[sy + y] + sx * mask_stride + mask_offset;
for (x = 0; x < xsize; x++) {
UINT8 in_alpha;
/* apply overall_alpha */
if (overall_alpha_int != 255 && *inmask != 0) {
in_alpha = MULDIV255(*inmask, overall_alpha_int, tmp1);
} else {
in_alpha = *inmask;
}
/* special cases */
if (in_alpha == 255 || *outmask == 0) {
*outmask = in_alpha;
*out = *in;
out++, in++;
*out = *in;
out++, in++;
*out = *in;
out++, in++;
} else if (in_alpha == 0) {
/* do nothing -- source is fully transparent */
out += 3;
in += 3;
} else {
/* general case */
int alpha = in_alpha + MULDIV255(*outmask, 255 - in_alpha, tmp1);
for (i = 0; i < 3; i++) {
/* general case */
*out = MULDIV255(*in, in_alpha, tmp1) +
MULDIV255(MULDIV255(*out, *outmask, tmp2), 255 - in_alpha, tmp3);
*out = (*out * 255) / alpha;
out++, in++;
}
*outmask = alpha;
}
out++;
if (src_has_alpha)
in++;
outmask += 4;
inmask += mask_stride;
}
}
return dest;
}
/* wraps alpha_over so it can be called directly from python */
/* properly refs the return value when needed: you DO need to decref the return */
PyObject *
alpha_over_wrap(PyObject *self, PyObject *args)
{
/* raw input python variables */
PyObject *dest, *src, *pos, *mask;
/* destination position and size */
int dx, dy, xsize, ysize;
/* return value: dest image on success */
PyObject *ret;
if (!PyArg_ParseTuple(args, "OOOO", &dest, &src, &pos, &mask))
return NULL;
/* destination position read */
if (!PyArg_ParseTuple(pos, "iiii", &dx, &dy, &xsize, &ysize)) {
/* try again, but this time try to read a point */
PyErr_Clear();
xsize = 0;
ysize = 0;
if (!PyArg_ParseTuple(pos, "ii", &dx, &dy)) {
PyErr_SetString(PyExc_TypeError,
"given blend destination rect is not valid");
return NULL;
}
}
ret = alpha_over(dest, src, mask, dx, dy, xsize, ysize);
if (ret == dest) {
/* Python needs us to own our return value */
Py_INCREF(dest);
}
return ret;
}
/* like alpha_over, but instead of src image it takes a source color
* also, it multiplies instead of doing an over operation
*/
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) {
/* libImaging handles */
Imaging imDest, imMask;
/* cached blend properties */
int mask_offset, mask_stride;
/* source position */
int sx, sy;
/* iteration variables */
unsigned int x, y;
/* temporary calculation variables */
int tmp1, tmp2;
imDest = imaging_python_to_c(dest);
imMask = imaging_python_to_c(mask);
if (!imDest || !imMask)
return NULL;
/* check the various image modes, make sure they make sense */
if (strcmp(imDest->mode, "RGBA") != 0) {
PyErr_SetString(PyExc_ValueError,
"given destination image does not have mode \"RGBA\"");
return NULL;
}
if (strcmp(imMask->mode, "RGBA") != 0 && strcmp(imMask->mode, "L") != 0) {
PyErr_SetString(PyExc_ValueError,
"given mask image does not have mode \"RGBA\" or \"L\"");
return NULL;
}
/* how far into image the first alpha byte resides */
mask_offset = (imMask->pixelsize == 4 ? 3 : 0);
/* how many bytes to skip to get to the next alpha byte */
mask_stride = imMask->pixelsize;
/* setup source & destination vars */
setup_source_destination(imMask, imDest, &sx, &sy, &dx, &dy, &xsize, &ysize);
/* check that there remains any blending to be done */
if (xsize <= 0 || ysize <= 0) {
/* nothing to do, return */
return dest;
}
for (y = 0; y < ysize; y++) {
UINT8 *out = (UINT8 *)imDest->image[dy + y] + dx * 4;
UINT8 *inmask = (UINT8 *)imMask->image[sy + y] + sx * mask_stride + mask_offset;
for (x = 0; x < xsize; x++) {
/* special cases */
if (*inmask == 255) {
*out = MULDIV255(*out, sr, tmp1);
out++;
*out = MULDIV255(*out, sg, tmp1);
out++;
*out = MULDIV255(*out, sb, tmp1);
out++;
*out = MULDIV255(*out, sa, tmp1);
out++;
} else if (*inmask == 0) {
/* do nothing -- source is fully transparent */
out += 4;
} else {
/* general case */
/* TODO work out general case */
*out = MULDIV255(*out, (255 - *inmask) + MULDIV255(sr, *inmask, tmp1), tmp2);
out++;
*out = MULDIV255(*out, (255 - *inmask) + MULDIV255(sg, *inmask, tmp1), tmp2);
out++;
*out = MULDIV255(*out, (255 - *inmask) + MULDIV255(sb, *inmask, tmp1), tmp2);
out++;
*out = MULDIV255(*out, (255 - *inmask) + MULDIV255(sa, *inmask, tmp1), tmp2);
out++;
}
inmask += mask_stride;
}
}
return dest;
}

39
src/endian.c Normal file
View File

@@ -0,0 +1,39 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
/* simple routines for dealing with endian conversion */
#define UNKNOWN_ENDIAN 0
#define BIG_ENDIAN 1
#define LITTLE_ENDIAN 2
static int endianness = UNKNOWN_ENDIAN;
void init_endian(void) {
/* figure out what our endianness is! */
short word = 0x0001;
char* byte = (char*)(&word);
endianness = byte[0] ? LITTLE_ENDIAN : BIG_ENDIAN;
}
unsigned short big_endian_ushort(unsigned short in) {
return (endianness == LITTLE_ENDIAN) ? ((in >> 8) | (in << 8)) : in;
}
unsigned int big_endian_uint(unsigned int in) {
return (endianness == LITTLE_ENDIAN) ? (((in & 0x000000FF) << 24) + ((in & 0x0000FF00) << 8) + ((in & 0x00FF0000) >> 8) + ((in & 0xFF000000) >> 24)) : in;
}

379
src/iterate.c Normal file
View File

@@ -0,0 +1,379 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
#include "overviewer.h"
static PyObject *textures = NULL;
static PyObject *chunk_mod = NULL;
static PyObject *blockmap = NULL;
static PyObject *special_blocks = NULL;
static PyObject *specialblockmap = NULL;
static PyObject *transparent_blocks = NULL;
int init_chunk_render(void) {
/* if blockmap (or any of these) is not NULL, then that means that we've
* somehow called this function twice. error out so we can notice this
* */
if (blockmap) return 1;
textures = PyImport_ImportModule("textures");
/* ensure none of these pointers are NULL */
if ((!textures)) {
fprintf(stderr, "\ninit_chunk_render failed to load; textures\n");
PyErr_Print();
return 1;
}
chunk_mod = PyImport_ImportModule("chunk");
/* ensure none of these pointers are NULL */
if ((!chunk_mod)) {
fprintf(stderr, "\ninit_chunk_render failed to load; chunk\n");
PyErr_Print();
return 1;
}
blockmap = PyObject_GetAttrString(textures, "blockmap");
special_blocks = PyObject_GetAttrString(textures, "special_blocks");
specialblockmap = PyObject_GetAttrString(textures, "specialblockmap");
transparent_blocks = PyObject_GetAttrString(chunk_mod, "transparent_blocks");
/* ensure none of these pointers are NULL */
if ((!transparent_blocks) || (!blockmap) || (!special_blocks) || (!specialblockmap)) {
fprintf(stderr, "\ninit_chunk_render failed\n");
PyErr_Print();
return 1;
}
return 0;
}
int
is_transparent(unsigned char b) {
PyObject *block = PyInt_FromLong(b);
int ret = PySequence_Contains(transparent_blocks, block);
Py_DECREF(block);
return ret;
}
unsigned char
check_adjacent_blocks(RenderState *state, int x,int y,int z, unsigned char blockid) {
/*
* Generates a pseudo ancillary data for blocks that depend of
* what are surrounded and don't have ancillary data. This
* function is through generate_pseudo_data.
*
* This uses a binary number of 4 digits to encode the info.
* The encode is:
*
* 0b1234:
* Bit: 1 2 3 4
* Side: +x +y -x -y
* Values: bit = 0 -> The corresponding side block has different blockid
* bit = 1 -> The corresponding side block has same blockid
* Example: if the bit1 is 1 that means that there is a block with
* blockid in the side of the +x direction.
*/
unsigned char pdata=0;
if (state->x == 15) { /* +x direction */
if (state->up_right_blocks != Py_None) { /* just in case we are in the end of the world */
if (getArrayByte3D(state->up_right_blocks, 0, y, z) == blockid) {
pdata = pdata|(1 << 3);
}
}
} else {
if (getArrayByte3D(state->blocks, x + 1, y, z) == blockid) {
pdata = pdata|(1 << 3);
}
}
if (state->y == 15) { /* +y direction*/
if (state->right_blocks != Py_None) {
if (getArrayByte3D(state->right_blocks, x, 0, z) == blockid) {
pdata = pdata|(1 << 2);
}
}
} else {
if (getArrayByte3D(state->blocks, x, y + 1, z) == blockid) {
pdata = pdata|(1 << 2);
}
}
if (state->x == 0) { /* -x direction*/
if (state->left_blocks != Py_None) {
if (getArrayByte3D(state->left_blocks, 15, y, z) == blockid) {
pdata = pdata|(1 << 1);
}
}
} else {
if (getArrayByte3D(state->blocks, x - 1, y, z) == blockid) {
pdata = pdata|(1 << 1);
}
}
if (state->y == 0) { /* -y direction */
if (state->up_left_blocks != Py_None) {
if (getArrayByte3D(state->up_left_blocks, x, 15, z) == blockid) {
pdata = pdata|(1 << 0);
}
}
} else {
if (getArrayByte3D(state->blocks, x, y - 1, z) == blockid) {
pdata = pdata|(1 << 0);
}
}
return pdata;
}
unsigned char
generate_pseudo_data(RenderState *state, unsigned char ancilData) {
/*
* Generates a fake ancillary data for blocks that are drawn
* depending on what are surrounded.
*/
int x = state->x, y = state->y, z = state->z;
unsigned char data = 0;
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;
return data; /* = 0b10000 */
} else if ((ancilData > 0) && (ancilData < 8)) { /* flowing water */
data = (check_adjacent_blocks(state, x, y, z, state->block) ^ 0x0f) | 0x10;
return data;
} else if ((ancilData == 8) || (ancilData == 9)) { /* falling water */
data = (check_adjacent_blocks(state, x, y, z, state->block) ^ 0x0f);
return data;
}
} else if (state->block == 85) { /* fences */
return check_adjacent_blocks(state, x, y, z, state->block);
} else if (state->block == 55) { /* redstone */
/* three addiotional bit are added, one for on/off state, and
* another two for going-up redstone wire in the same block
* (connection with the level z+1) */
unsigned char above_level_data = 0, same_level_data = 0, below_level_data = 0, possibly_connected = 0, final_data = 0;
/* check for air in z+1, no air = no connection with upper level */
if ((z != 127) && (getArrayByte3D(state->left_blocks, x, y, z) == 0)) {
above_level_data = check_adjacent_blocks(state, x, y, z + 1, state->block);
} /* else above_level_data = 0 */
/* check connection with same level */
same_level_data = check_adjacent_blocks(state, x, y, z, 55);
/* check the posibility of connection with z-1 level, check for air */
possibly_connected = check_adjacent_blocks(state, x, y, z, 0);
/* check connection with z-1 level */
if (z != 0) {
below_level_data = check_adjacent_blocks(state, x, y, z - 1, state->block);
} /* else below_level_data = 0 */
final_data = above_level_data | same_level_data | (below_level_data & possibly_connected);
/* add the three bits */
if (ancilData > 0) { /* powered redstone wire */
final_data = final_data | 0x40;
}
if ((above_level_data & 0x01)) { /* draw top left going up redstonewire */
final_data = final_data | 0x20;
}
if ((above_level_data & 0x08)) { /* draw top right going up redstonewire */
final_data = final_data | 0x10;
}
return final_data;
}
return 0;
}
/* TODO triple check this to make sure reference counting is correct */
PyObject*
chunk_render(PyObject *self, PyObject *args) {
RenderState state;
PyObject *blockdata_expanded;
int xoff, yoff;
PyObject *imgsize, *imgsize0_py, *imgsize1_py;
int imgsize0, imgsize1;
PyObject *blocks_py;
PyObject *left_blocks_py;
PyObject *right_blocks_py;
PyObject *up_left_blocks_py;
PyObject *up_right_blocks_py;
RenderModeInterface *rendermode;
void *rm_data;
PyObject *t = NULL;
if (!PyArg_ParseTuple(args, "OOiiO", &state.self, &state.img, &xoff, &yoff, &blockdata_expanded))
return Py_BuildValue("i", "-1");
/* fill in important modules */
state.textures = textures;
state.chunk = chunk_mod;
/* set up the render mode */
rendermode = get_render_mode(&state);
rm_data = calloc(1, rendermode->data_size);
if (rendermode->start(rm_data, &state)) {
free(rm_data);
return Py_BuildValue("i", "-1");
}
/* get the image size */
imgsize = PyObject_GetAttrString(state.img, "size");
imgsize0_py = PySequence_GetItem(imgsize, 0);
imgsize1_py = PySequence_GetItem(imgsize, 1);
Py_DECREF(imgsize);
imgsize0 = PyInt_AsLong(imgsize0_py);
imgsize1 = PyInt_AsLong(imgsize1_py);
Py_DECREF(imgsize0_py);
Py_DECREF(imgsize1_py);
/* get the block data directly from numpy: */
blocks_py = PyObject_GetAttrString(state.self, "blocks");
state.blocks = blocks_py;
left_blocks_py = PyObject_GetAttrString(state.self, "left_blocks");
state.left_blocks = left_blocks_py;
right_blocks_py = PyObject_GetAttrString(state.self, "right_blocks");
state.right_blocks = right_blocks_py;
up_left_blocks_py = PyObject_GetAttrString(state.self, "up_left_blocks");
state.up_left_blocks = up_left_blocks_py;
up_right_blocks_py = PyObject_GetAttrString(state.self, "up_right_blocks");
state.up_right_blocks = up_right_blocks_py;
for (state.x = 15; state.x > -1; state.x--) {
for (state.y = 0; state.y < 16; state.y++) {
PyObject *blockid = NULL;
/* set up the render coordinates */
state.imgx = xoff + state.x*12 + state.y*12;
/* 128*12 -- offset for z direction, 15*6 -- offset for x */
state.imgy = yoff - state.x*6 + state.y*6 + 128*12 + 15*6;
for (state.z = 0; state.z < 128; state.z++) {
state.imgy -= 12;
/* make sure we're rendering inside the image boundaries */
if ((state.imgx >= imgsize0 + 24) || (state.imgx <= -24)) {
continue;
}
if ((state.imgy >= imgsize1 + 24) || (state.imgy <= -24)) {
continue;
}
/* get blockid */
state.block = getArrayByte3D(blocks_py, state.x, state.y, state.z);
if (state.block == 0) {
continue;
}
/* decref'd on replacement *and* at the end of the z for block */
if (blockid) {
Py_DECREF(blockid);
}
blockid = PyInt_FromLong(state.block);
// check for occlusion
if (rendermode->occluded(rm_data, &state)) {
continue;
}
// everything stored here will be a borrowed ref
/* get the texture and mask from block type / ancil. data */
if (!PySequence_Contains(special_blocks, blockid)) {
/* t = textures.blockmap[blockid] */
t = PyList_GetItem(blockmap, state.block);
} else {
PyObject *tmp;
unsigned char ancilData = getArrayByte3D(blockdata_expanded, state.x, state.y, state.z);
if ((state.block == 85) || (state.block == 9) || (state.block == 55)) {
ancilData = generate_pseudo_data(&state, ancilData);
}
tmp = PyTuple_New(2);
Py_INCREF(blockid); /* because SetItem steals */
PyTuple_SetItem(tmp, 0, blockid);
PyTuple_SetItem(tmp, 1, PyInt_FromLong(ancilData));
/* this is a borrowed reference. no need to decref */
t = PyDict_GetItem(specialblockmap, tmp);
Py_DECREF(tmp);
}
/* if we found a proper texture, render it! */
if (t != NULL && t != Py_None)
{
PyObject *src, *mask;
src = PyTuple_GetItem(t, 0);
mask = PyTuple_GetItem(t, 1);
if (mask == Py_None)
mask = src;
rendermode->draw(rm_data, &state, src, mask);
}
}
if (blockid) {
Py_DECREF(blockid);
blockid = NULL;
}
}
}
/* free up the rendermode info */
rendermode->finish(rm_data, &state);
free(rm_data);
Py_DECREF(blocks_py);
Py_XDECREF(left_blocks_py);
Py_XDECREF(right_blocks_py);
Py_XDECREF(up_left_blocks_py);
Py_XDECREF(up_right_blocks_py);
return Py_BuildValue("i",2);
}

64
src/main.c Normal file
View File

@@ -0,0 +1,64 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
#include "overviewer.h"
PyObject *get_extension_version(PyObject *self, PyObject *args) {
return Py_BuildValue("i", OVERVIEWER_EXTENSION_VERSION);
}
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 */
};
PyMODINIT_FUNC
initc_overviewer(void)
{
(void)Py_InitModule("c_overviewer", COverviewerMethods);
/* for numpy */
import_array();
/* initialize some required variables in iterage.c */
if (init_chunk_render()) {
fprintf(stderr, "failed to init_chunk_render\n");
exit(1); // TODO better way to indicate error?
}
init_endian();
}

91
src/overviewer.h Normal file
View File

@@ -0,0 +1,91 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
/*
* This is a general include file for the Overviewer C extension. It
* lists useful, defined functions as well as those that are exported
* to python, so all files can use them.
*/
#ifndef __OVERVIEWER_H_INCLUDED__
#define __OVERVIEWER_H_INCLUDED__
// increment this value if you've made a change to the c extesion
// and want to force users to rebuild
#define OVERVIEWER_EXTENSION_VERSION 4
/* Python PIL, and numpy headers */
#include <Python.h>
#include <Imaging.h>
#include <numpy/arrayobject.h>
/* macro for getting a value out of various numpy arrays */
#define getArrayByte3D(array, x,y,z) (*(unsigned char *)(PyArray_GETPTR3((array), (x), (y), (z))))
#define getArrayShort1D(array, x) (*(unsigned short *)(PyArray_GETPTR1((array), (x))))
/* generally useful MAX / MIN macros */
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define MIN(a, b) ((a) < (b) ? (a) : (b))
/* in composite.c */
Imaging imaging_python_to_c(PyObject *obj);
PyObject *alpha_over(PyObject *dest, PyObject *src, PyObject *mask,
int dx, int dy, int xsize, int ysize);
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, unsigned char sa,
PyObject *mask, int dx, int dy, int xsize, int ysize);
/* in iterate.c */
typedef struct {
/* the ChunkRenderer object */
PyObject *self;
/* important modules, for convenience */
PyObject *textures;
PyObject *chunk;
/* the rest only make sense for occluded() and draw() !! */
/* the tile image and destination */
PyObject *img;
int imgx, imgy;
/* the block position and type, and the block array */
int x, y, z;
unsigned char block;
PyObject *blocks;
PyObject *up_left_blocks;
PyObject *up_right_blocks;
PyObject *left_blocks;
PyObject *right_blocks;
} RenderState;
int init_chunk_render(void);
int is_transparent(unsigned char b);
PyObject *chunk_render(PyObject *self, PyObject *args);
/* pull in the rendermode info */
#include "rendermodes.h"
/* in endian.c */
void init_endian(void);
unsigned short big_endian_ushort(unsigned short in);
unsigned int big_endian_uint(unsigned int in);
#endif /* __OVERVIEWER_H_INCLUDED__ */

234
src/rendermode-cave.c Normal file
View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
#include "overviewer.h"
#include <math.h>
//~
//~ /* 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,
};

238
src/rendermode-lighting.c Normal file
View File

@@ -0,0 +1,238 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
#include "overviewer.h"
#include <math.h>
/* figures out the black_coeff from a given skylight and blocklight,
used in lighting calculations */
static float calculate_darkness(unsigned char skylight, unsigned char blocklight) {
return 1.0f - powf(0.8f, 15.0 - MAX(blocklight, skylight));
}
/* loads the appropriate light data for the given (possibly non-local)
* coordinates, and returns a black_coeff this is exposed, so other (derived)
* rendermodes can use it
*
* authoratative is a return slot for whether or not this lighting calculation
* is true, or a guess. If we guessed, *authoratative will be false, but if it
* was calculated correctly from available light data, it will be true. You
* may (and probably should) pass NULL.
*/
inline float
get_lighting_coefficient(RenderModeLighting *self, RenderState *state,
int x, int y, int z, int *authoratative) {
/* placeholders for later data arrays, coordinates */
PyObject *blocks = NULL;
PyObject *skylight = NULL;
PyObject *blocklight = NULL;
int local_x = x, local_y = y, local_z = z;
unsigned char block, skylevel, blocklevel;
/* defaults to "guess" until told otherwise */
if (authoratative)
*authoratative = 0;
/* find out what chunk we're in, and translate accordingly */
if (x >= 0 && y < 16) {
blocks = state->blocks;
skylight = self->skylight;
blocklight = self->blocklight;
} else if (x < 0) {
local_x += 16;
blocks = state->left_blocks;
skylight = self->left_skylight;
blocklight = self->left_blocklight;
} else if (y >= 16) {
local_y -= 16;
blocks = state->right_blocks;
skylight = self->right_skylight;
blocklight = self->right_blocklight;
}
/* make sure we have correctly-ranged coordinates */
if (!(local_x >= 0 && local_x < 16 &&
local_y >= 0 && local_y < 16 &&
local_z >= 0 && local_z < 128)) {
return self->calculate_darkness(15, 0);
}
/* also, make sure we have enough info to correctly calculate lighting */
if (blocks == Py_None || blocks == NULL ||
skylight == Py_None || skylight == NULL ||
blocklight == Py_None || blocklight == NULL) {
return self->calculate_darkness(15, 0);
}
block = getArrayByte3D(blocks, local_x, local_y, local_z);
/* if this block is opaque, use a fully-lit coeff instead
to prevent stippled lines along chunk boundaries! */
if (!is_transparent(block)) {
return self->calculate_darkness(15, 0);
}
/* only do special half-step handling if no authoratative pointer was
passed in, which is a sign that we're recursing */
if (block == 44 && authoratative == NULL) {
float average_gather = 0.0f;
unsigned int average_count = 0;
int auth;
float coeff;
/* iterate through all surrounding blocks to take an average */
int dx, dy, dz;
for (dx = -1; dx <= 1; dx += 2) {
for (dy = -1; dy <= 1; dy += 2) {
for (dz = -1; dz <= 1; dz += 2) {
coeff = get_lighting_coefficient(self, state, x+dx, y+dy, z+dz, &auth);
if (auth) {
average_gather += coeff;
average_count++;
}
}
}
}
/* only return the average if at least one was authoratative */
if (average_count > 0)
return average_gather / average_count;
}
if (block == 10 || block == 11) {
/* lava blocks should always be lit! */
return 0.0f;
}
skylevel = getArrayByte3D(skylight, local_x, local_y, local_z);
blocklevel = getArrayByte3D(blocklight, local_x, local_y, local_z);
/* no longer a guess */
if (authoratative)
*authoratative = 1;
return self->calculate_darkness(skylevel, blocklevel);
}
/* shades the drawn block with the given facemask/black_color, based on the
lighting results from (x, y, z) */
static inline void
do_shading_with_mask(RenderModeLighting *self, RenderState *state,
int x, int y, int z, PyObject *mask) {
float black_coeff;
/* first, check for occlusion if the block is in the local chunk */
if (x >= 0 && x < 16 && y >= 0 && y < 16 && z >= 0 && z < 128) {
unsigned char block = getArrayByte3D(state->blocks, x, y, z);
if (!is_transparent(block)) {
/* this face isn't visible, so don't draw anything */
return;
}
}
black_coeff = get_lighting_coefficient(self, state, x, y, z, NULL);
alpha_over_full(state->img, self->black_color, mask, black_coeff, state->imgx, state->imgy, 0, 0);
}
static int
rendermode_lighting_start(void *data, RenderState *state) {
RenderModeLighting* self;
/* first, chain up */
int ret = rendermode_normal.start(data, state);
if (ret != 0)
return ret;
self = (RenderModeLighting *)data;
self->black_color = PyObject_GetAttrString(state->chunk, "black_color");
self->facemasks_py = PyObject_GetAttrString(state->chunk, "facemasks");
// borrowed references, don't need to be decref'd
self->facemasks[0] = PyTuple_GetItem(self->facemasks_py, 0);
self->facemasks[1] = PyTuple_GetItem(self->facemasks_py, 1);
self->facemasks[2] = PyTuple_GetItem(self->facemasks_py, 2);
self->skylight = PyObject_GetAttrString(state->self, "skylight");
self->blocklight = PyObject_GetAttrString(state->self, "blocklight");
self->left_skylight = PyObject_GetAttrString(state->self, "left_skylight");
self->left_blocklight = PyObject_GetAttrString(state->self, "left_blocklight");
self->right_skylight = PyObject_GetAttrString(state->self, "right_skylight");
self->right_blocklight = PyObject_GetAttrString(state->self, "right_blocklight");
self->calculate_darkness = calculate_darkness;
return 0;
}
static void
rendermode_lighting_finish(void *data, RenderState *state) {
RenderModeLighting *self = (RenderModeLighting *)data;
Py_DECREF(self->black_color);
Py_DECREF(self->facemasks_py);
Py_DECREF(self->skylight);
Py_DECREF(self->blocklight);
Py_DECREF(self->left_skylight);
Py_DECREF(self->left_blocklight);
Py_DECREF(self->right_skylight);
Py_DECREF(self->right_blocklight);
/* now chain up */
rendermode_normal.finish(data, state);
}
static int
rendermode_lighting_occluded(void *data, RenderState *state) {
/* no special occlusion here */
return rendermode_normal.occluded(data, state);
}
static void
rendermode_lighting_draw(void *data, RenderState *state, PyObject *src, PyObject *mask) {
RenderModeLighting* self;
int x, y, z;
/* first, chain up */
rendermode_normal.draw(data, state, src, mask);
self = (RenderModeLighting *)data;
x = state->x, y = state->y, z = state->z;
if (is_transparent(state->block)) {
/* transparent: do shading on whole block */
do_shading_with_mask(self, state, x, y, z, mask);
} else {
/* opaque: do per-face shading */
do_shading_with_mask(self, state, x, y, z+1, self->facemasks[0]);
do_shading_with_mask(self, state, x-1, y, z, self->facemasks[1]);
do_shading_with_mask(self, state, x, y+1, z, self->facemasks[2]);
}
}
RenderModeInterface rendermode_lighting = {
"lighting", "draw shadows from the lighting data",
&rendermode_normal,
sizeof(RenderModeLighting),
rendermode_lighting_start,
rendermode_lighting_finish,
rendermode_lighting_occluded,
rendermode_lighting_draw,
};

70
src/rendermode-night.c Normal file
View File

@@ -0,0 +1,70 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
#include "overviewer.h"
#include <math.h>
/* 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_night_start(void *data, RenderState *state) {
RenderModeNight* self;
/* first, chain up */
int ret = rendermode_lighting.start(data, state);
if (ret != 0)
return ret;
/* override the darkness function with our night version! */
self = (RenderModeNight *)data;
self->parent.calculate_darkness = calculate_darkness;
return 0;
}
static void
rendermode_night_finish(void *data, RenderState *state) {
/* nothing special to do */
rendermode_lighting.finish(data, state);
}
static int
rendermode_night_occluded(void *data, RenderState *state) {
/* no special occlusion here */
return rendermode_lighting.occluded(data, state);
}
static void
rendermode_night_draw(void *data, RenderState *state, PyObject *src, PyObject *mask) {
/* nothing special to do */
rendermode_lighting.draw(data, state, src, mask);
}
RenderModeInterface rendermode_night = {
"night", "like \"lighting\", except at night",
&rendermode_lighting,
sizeof(RenderModeNight),
rendermode_night_start,
rendermode_night_finish,
rendermode_night_occluded,
rendermode_night_draw,
};

230
src/rendermode-normal.c Normal file
View File

@@ -0,0 +1,230 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
#include "overviewer.h"
static int
rendermode_normal_start(void *data, RenderState *state) {
PyObject *chunk_x_py, *chunk_y_py, *world, *use_biomes, *worlddir;
RenderModeNormal *self = (RenderModeNormal *)data;
chunk_x_py = PyObject_GetAttrString(state->self, "chunkX");
chunk_y_py = PyObject_GetAttrString(state->self, "chunkY");
/* careful now -- C's % operator works differently from python's
we can't just do x % 32 like we did before */
self->chunk_x = PyInt_AsLong(chunk_x_py);
self->chunk_y = PyInt_AsLong(chunk_y_py);
while (self->chunk_x < 0)
self->chunk_x += 32;
while (self->chunk_y < 0)
self->chunk_y += 32;
self->chunk_x %= 32;
self->chunk_y %= 32;
/* fetch the biome data from textures.py, if needed */
world = PyObject_GetAttrString(state->self, "world");
worlddir = PyObject_GetAttrString(world, "worlddir");
use_biomes = PyObject_GetAttrString(world, "useBiomeData");
Py_DECREF(world);
if (PyObject_IsTrue(use_biomes)) {
PyObject *facemasks_py;
self->biome_data = PyObject_CallMethod(state->textures, "getBiomeData", "OOO",
worlddir, chunk_x_py, chunk_y_py);
if (self->biome_data == Py_None) {
self->biome_data = NULL;
self->foliagecolor = NULL;
self->grasscolor = NULL;
self->leaf_texture = NULL;
self->grass_texture = NULL;
self->facemask_top = NULL;
} else {
self->foliagecolor = PyObject_GetAttrString(state->textures, "foliagecolor");
self->grasscolor = PyObject_GetAttrString(state->textures, "grasscolor");
self->leaf_texture = PyObject_GetAttrString(state->textures, "biome_leaf_texture");
self->grass_texture = PyObject_GetAttrString(state->textures, "biome_grass_texture");
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);
}
} else {
self->biome_data = NULL;
self->foliagecolor = NULL;
self->grasscolor = NULL;
self->leaf_texture = NULL;
self->grass_texture = NULL;
self->facemask_top = NULL;
}
Py_DECREF(use_biomes);
Py_DECREF(worlddir);
Py_DECREF(chunk_x_py);
Py_DECREF(chunk_y_py);
return 0;
}
static void
rendermode_normal_finish(void *data, RenderState *state) {
RenderModeNormal *self = (RenderModeNormal *)data;
Py_XDECREF(self->biome_data);
Py_XDECREF(self->foliagecolor);
Py_XDECREF(self->grasscolor);
Py_XDECREF(self->leaf_texture);
Py_XDECREF(self->grass_texture);
Py_XDECREF(self->facemask_top);
}
static int
rendermode_normal_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_normal_draw(void *data, RenderState *state, PyObject *src, PyObject *mask) {
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;
};
}
/* draw the block! */
alpha_over(state->img, src, mask, state->imgx, state->imgy, 0, 0);
if (self->biome_data) {
/* do the biome stuff! */
unsigned int index;
PyObject *color = NULL, *facemask = NULL;
unsigned char r, g, b;
index = ((self->chunk_y * 16) + state->y) * 16 * 32 + (self->chunk_x * 16) + state->x;
index = big_endian_ushort(getArrayShort1D(self->biome_data, index));
switch (state->block) {
case 2:
/* grass */
color = PySequence_GetItem(self->grasscolor, index);
facemask = self->facemask_top;
break;
case 18:
/* leaves */
color = PySequence_GetItem(self->foliagecolor, index);
facemask = mask;
break;
default:
break;
};
if (color)
{
/* we've got work to do */
r = PyInt_AsLong(PyTuple_GET_ITEM(color, 0));
g = PyInt_AsLong(PyTuple_GET_ITEM(color, 1));
b = PyInt_AsLong(PyTuple_GET_ITEM(color, 2));
Py_DECREF(color);
tint_with_mask(state->img, r, g, b, 255, facemask, state->imgx, state->imgy, 0, 0);
}
}
/* Draw some edge lines! */
// draw.line(((imgx+12,imgy+increment), (imgx+22,imgy+5+increment)), fill=(0,0,0), width=1)
if (state->block == 44 || state->block == 78 || !is_transparent(state->block)) {
Imaging img_i = imaging_python_to_c(state->img);
unsigned char ink[] = {0,0,0,40};
int increment=0;
if (state->block == 44) // half-step
increment=6;
else if (state->block == 78) // snow
increment=9;
if ((state->x == 15) && (state->up_right_blocks != Py_None)) {
unsigned char side_block = getArrayByte3D(state->up_right_blocks, 0, state->y, state->z);
if (side_block != state->block && is_transparent(side_block)) {
ImagingDrawLine(img_i, state->imgx+12, state->imgy+1+increment, state->imgx+22+1, state->imgy+5+1+increment, &ink, 1);
ImagingDrawLine(img_i, state->imgx+12, state->imgy+increment, state->imgx+22+1, state->imgy+5+increment, &ink, 1);
}
} else if (state->x != 15) {
unsigned char side_block = getArrayByte3D(state->blocks, state->x+1, state->y, state->z);
if (side_block != state->block && is_transparent(side_block)) {
ImagingDrawLine(img_i, state->imgx+12, state->imgy+1+increment, state->imgx+22+1, state->imgy+5+1+increment, &ink, 1);
ImagingDrawLine(img_i, state->imgx+12, state->imgy+increment, state->imgx+22+1, state->imgy+5+increment, &ink, 1);
}
}
// if y != 0 and blocks[x,y-1,z] == 0
// chunk boundries are annoying
if ((state->y == 0) && (state->up_left_blocks != Py_None)) {
unsigned char side_block = getArrayByte3D(state->up_left_blocks, state->x, 15, state->z);
if (side_block != state->block && is_transparent(side_block)) {
ImagingDrawLine(img_i, state->imgx, state->imgy+6+1+increment, state->imgx+12+1, state->imgy+1+increment, &ink, 1);
ImagingDrawLine(img_i, state->imgx, state->imgy+6+increment, state->imgx+12+1, state->imgy+increment, &ink, 1);
}
} else if (state->y != 0) {
unsigned char side_block = getArrayByte3D(state->blocks, state->x, state->y-1, state->z);
if (side_block != state->block && is_transparent(side_block)) {
// draw.line(((imgx,imgy+6+increment), (imgx+12,imgy+increment)), fill=(0,0,0), width=1)
ImagingDrawLine(img_i, state->imgx, state->imgy+6+1+increment, state->imgx+12+1, state->imgy+1+increment, &ink, 1);
ImagingDrawLine(img_i, state->imgx, state->imgy+6+increment, state->imgx+12+1, state->imgy+increment, &ink, 1);
}
}
}
}
RenderModeInterface rendermode_normal = {
"normal", "nothing special, just render the blocks",
NULL,
sizeof(RenderModeNormal),
rendermode_normal_start,
rendermode_normal_finish,
rendermode_normal_occluded,
rendermode_normal_draw,
};

137
src/rendermode-overlay.c Normal file
View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
#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,
};

118
src/rendermode-spawn.c Normal file
View File

@@ -0,0 +1,118 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
#include "overviewer.h"
#include <math.h>
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_overlay.start(data, state);
if (ret != 0)
return ret;
/* now do custom initializations */
self = (RenderModeSpawn *)data;
self->nospawn_blocks = PyObject_GetAttrString(state->chunk, "nospawn_blocks");
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;
}
static void
rendermode_spawn_finish(void *data, RenderState *state) {
/* first free all *our* stuff */
RenderModeSpawn* self = (RenderModeSpawn *)data;
Py_DECREF(self->nospawn_blocks);
Py_DECREF(self->blocklight);
Py_DECREF(self->skylight);
/* now, chain up */
rendermode_overlay.finish(data, state);
}
static int
rendermode_spawn_occluded(void *data, RenderState *state) {
/* no special occlusion here */
return rendermode_overlay.occluded(data, state);
}
static void
rendermode_spawn_draw(void *data, RenderState *state, PyObject *src, PyObject *mask) {
/* draw normally */
rendermode_overlay.draw(data, state, src, mask);
}
RenderModeInterface rendermode_spawn = {
"spawn", "draws a red overlay where monsters can spawn at night",
&rendermode_overlay,
sizeof(RenderModeSpawn),
rendermode_spawn_start,
rendermode_spawn_finish,
rendermode_spawn_occluded,
rendermode_spawn_draw,
};

184
src/rendermodes.c Normal file
View File

@@ -0,0 +1,184 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
#include "overviewer.h"
#include <string.h>
/* list of all render modes, ending in NULL
all of these will be available to the user, so DON'T include modes
that are only useful as a base for other modes. */
static RenderModeInterface *render_modes[] = {
&rendermode_normal,
&rendermode_lighting,
&rendermode_night,
&rendermode_spawn,
&rendermode_cave,
NULL
};
/* decides which render mode to use */
RenderModeInterface *get_render_mode(RenderState *state) {
unsigned int i;
/* default: NULL --> an error */
RenderModeInterface *iface = NULL;
PyObject *rendermode_py = PyObject_GetAttrString(state->self, "rendermode");
const char *rendermode = PyString_AsString(rendermode_py);
for (i = 0; render_modes[i] != NULL; i++) {
if (strcmp(render_modes[i]->name, rendermode) == 0) {
iface = render_modes[i];
break;
}
}
Py_DECREF(rendermode_py);
return iface;
}
/* bindings for python -- get all the rendermode names */
PyObject *get_render_modes(PyObject *self, PyObject *args) {
PyObject *modes;
unsigned int i;
if (!PyArg_ParseTuple(args, ""))
return NULL;
modes = PyList_New(0);
if (modes == NULL)
return NULL;
for (i = 0; render_modes[i] != NULL; i++) {
PyObject *name = PyString_FromString(render_modes[i]->name);
PyList_Append(modes, name);
Py_DECREF(name);
}
return modes;
}
/* more bindings -- return info for a given rendermode name */
PyObject *get_render_mode_info(PyObject *self, PyObject *args) {
const char* rendermode;
PyObject *info;
unsigned int i;
if (!PyArg_ParseTuple(args, "s", &rendermode))
return NULL;
info = PyDict_New();
if (info == NULL)
return NULL;
for (i = 0; render_modes[i] != NULL; i++) {
if (strcmp(render_modes[i]->name, rendermode) == 0) {
PyObject *tmp;
tmp = PyString_FromString(render_modes[i]->name);
PyDict_SetItemString(info, "name", tmp);
Py_DECREF(tmp);
tmp = PyString_FromString(render_modes[i]->description);
PyDict_SetItemString(info, "description", tmp);
Py_DECREF(tmp);
return info;
}
}
Py_DECREF(info);
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;
}

159
src/rendermodes.h Normal file
View File

@@ -0,0 +1,159 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
/*
* To make a new render mode (the C part, at least):
*
* * add a data struct and extern'd interface declaration below
*
* * fill in this interface struct in rendermode-(yourmode).c
* (see rendermodes-normal.c for an example: the "normal" mode)
*
* * if you want to derive from (say) the "normal" mode, put
* a RenderModeNormal entry at the top of your data struct, and
* be sure to call your parent's functions in your own!
* (see rendermode-night.c for a simple example derived from
* the "lighting" mode)
*
* * add your mode to the list in rendermodes.c
*/
#ifndef __RENDERMODES_H_INCLUDED__
#define __RENDERMODES_H_INCLUDED__
#include <Python.h>
/* rendermode interface */
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;
/* may return non-zero on error */
int (*start)(void *, RenderState *);
void (*finish)(void *, RenderState *);
/* returns non-zero to skip rendering this block */
int (*occluded)(void *, RenderState *);
/* last two arguments are img and mask, from texture lookup */
void (*draw)(void *, RenderState *, PyObject *, PyObject *);
};
/* 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 */
/* NORMAL */
typedef struct {
/* coordinates of the chunk, inside its region file */
int chunk_x, chunk_y;
/* biome data for the region */
PyObject *biome_data;
/* grasscolor and foliagecolor lookup tables */
PyObject *grasscolor, *foliagecolor;
/* biome-compatible grass/leaf textures */
PyObject *grass_texture, *leaf_texture;
/* top facemask for grass biome tinting */
PyObject *facemask_top;
} 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 */
RenderModeNormal parent;
PyObject *black_color, *facemasks_py;
PyObject *facemasks[3];
/* extra data, loaded off the chunk class */
PyObject *skylight, *blocklight;
PyObject *left_skylight, *left_blocklight;
PyObject *right_skylight, *right_blocklight;
/* can be overridden in derived rendermodes to control lighting
arguments are skylight, blocklight */
float (*calculate_darkness)(unsigned char, unsigned char);
} RenderModeLighting;
extern RenderModeInterface rendermode_lighting;
inline float get_lighting_coefficient(RenderModeLighting *self, RenderState *state,
int x, int y, int z, int *authoratative);
/* NIGHT */
typedef struct {
/* inherits from lighting */
RenderModeLighting parent;
} RenderModeNight;
extern RenderModeInterface rendermode_night;
/* SPAWN */
typedef struct {
/* inherits from overlay */
RenderModeOverlay parent;
/* used to figure out which blocks are spawnable */
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__ */

File diff suppressed because it is too large Load Diff

21
util.py
View File

@@ -30,3 +30,24 @@ def get_program_path():
return os.path.dirname(__file__)
except NameError:
return os.path.dirname(sys.argv[0])
def findGitVersion():
this_dir = get_program_path()
if os.path.exists(os.path.join(this_dir,".git")):
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, ".git", data[5:])):
return data
with open(os.path.join(this_dir, ".git", data[5:])) as g:
return g.read().strip()
else:
return data
else:
try:
import overviewer_version
return overviewer_version.VERSION
except:
return "unknown"

View File

@@ -5,6 +5,7 @@ function prepareSignMarker(marker, item) {
var c = "<div class=\"infoWindow\"><img src=\"signpost.png\" /><p>" + item.msg.replace(/\n/g,"<br/>") + "</p></div>";
var infowindow = new google.maps.InfoWindow({content: c
});
marker.infowindow = infowindow
google.maps.event.addListener(marker, 'click', function() {
if (prevInfoWindow)
prevInfoWindow.close()
@@ -238,27 +239,72 @@ function initMarkers() {
}
}
var oldHash = "";
function makeLink() {
location.hash = "#/"+map.getCenter().lat().toFixed(3)+"/"+map.getCenter().lng().toFixed(3)+"/"+map.getZoom();
function setHash() {
var hard_permalink = isHardPermalink;
var hash = window.location.hash
var params = hash.split("/")
if (params.length > 1) {
if (permalinks[params[1]]) {
hard_permalink = true;
}
}
//if (!hard_permalink) {
location.hash = "#/"+map.getCenter().lat().toFixed(3)+"/"+map.getCenter().lng().toFixed(3)+"/"+map.getZoom();
oldHash = location.hash
//}
}
var permalinks = {
"rent": "Rent is $200/month jeff@ unterbahn.com",
}
function openSign(markerName) {
var marker = null;
for (i=0;i<markerCollection.All.length;i++) {
if (markerCollection.All[i].title == permalinks[markerName]) marker = markerCollection.All[i];
}
if (marker) {
if (prevInfoWindow)
prevInfoWindow.close()
marker.infowindow.open(map,marker);
prevInfoWindow = marker.infowindow
}
}
var isHardPermalink = false
document.addEventListener("mousedown",function() {isHardPermalink = false})
function readHash(mapOptions) {
var hash = window.location.hash
var params = hash.split("/")
if (params.length > 1) {
if (params[1] == "rent") {
mapOptions = {lat: 0.546875, lng: 0.48046875, zoom: 6}
isHardPermalink = true
if (markerCollection.All) openSign("rent");
} else {
mapOptions = mapOptions || {}
mapOptions.lat = parseFloat(params[1])
mapOptions.lng = parseFloat(params[2])
mapOptions.zoom = parseInt(params[3])
}
if (map) map.setCenter(new google.maps.LatLng(mapOptions.lat, mapOptions.lng))
if (map) map.setZoom(mapOptions.zoom)
}
}
var map = null;
function initialize() {
var query = location.search.substring(1);
var lat = 0.5;
var lng = 0.5;
var lat = 0.547;
var lng = 0.460;
var zoom = config.defaultZoom;
// Jeff's hash rewriting:
var hash = window.location.hash
var params = hash.split("/")
if (params.length > 1) {
lat = parseFloat(params[1])
lng = parseFloat(params[2])
zoom = parseInt(params[3])
}
setInterval(readHash,500);
var mapTyepControlToggle = false
if (mapTypeIds.length > 1) {
@@ -277,6 +323,7 @@ function initialize() {
streetViewControl: false,
backgroundColor: config.bg_color,
};
readHash(mapOptions);
map = new google.maps.Map(document.getElementById('mcmap'), mapOptions);
if(config.debug) {
@@ -307,14 +354,14 @@ function initialize() {
initRegions();
drawMapControls();
//makeLink();
readHash(mapOptions);
// Make the link again whenever the map changes
google.maps.event.addListener(map, 'zoom_changed', function() {
makeLink();
setHash();
});
google.maps.event.addListener(map, 'center_changed', function() {
makeLink();
setHash();
});
}

View File

@@ -2,18 +2,16 @@
<html>
<head>
<meta name="viewport" content="initial-scale=1.0, user-scalable=no" />
<link rel="stylesheet" href="style.css" type="text/css" />
<script type="text/javascript" src="config.js"></script>
<script type="text/javascript"
src="http://maps.google.com/maps/api/js?sensor=false">
</script>
<link rel="stylesheet" href="overviewer.css" type="text/css" />
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.3/jquery.min.js"></script>
<script type="text/javascript" src="functions.js"></script>
<script type="text/javascript" src="http://maps.google.com/maps/api/js?sensor=false"></script>
<script type="text/javascript" src="overviewerConfig.js"></script>
<script type="text/javascript" src="overviewer.js"></script>
<script type="text/javascript" src="markers.js"></script>
<script type="text/javascript" src="regions.js"></script>
</head>
<!-- Generated at: {time} -->
<body onload="initialize()">
<div id="mcmap" style="width:100%; height:100%"></div>
<body onload="overviewer.util.initialize()">
<div id="mcmap"></div>
</body>
</html>

View File

@@ -1,9 +1,25 @@
html {
height: 100%;
<<<<<<< HEAD:web_assets/style.css
font-family: lucida grande, lucida sans console, sans-serif;
}
body { height: 100%; margin: 0px; padding: 0px ; background-color: #000; }
#mcmap { height: 100% }
=======
}
body {
height: 100%;
margin: 0px;
padding: 0px;
background-color: #000;
}
#mcmap {
width: 100%;
height: 100%;
}
>>>>>>> f20d81c39f0302a44e02a6f42f6eb0d13b9fcc18:web_assets/overviewer.css
#mcmap div div a img, #mcmap div div div a {
display:none;
@@ -24,13 +40,17 @@ body { height: 100%; margin: 0px; padding: 0px ; background-color: #000; }
font-family: monospace;
}
#signControl {
#customControl {
padding: 5px;
height: 15px;
}
<<<<<<< HEAD:web_assets/style.css
#signControl > div#top {
display:none;
=======
#customControl > div#top {
>>>>>>> f20d81c39f0302a44e02a6f42f6eb0d13b9fcc18:web_assets/overviewer.css
background-color: #fff;
border: 2px solid #000;
text-align: center;
@@ -40,7 +60,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;

893
web_assets/overviewer.js Normal file
View File

@@ -0,0 +1,893 @@
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([<your list of markers>]);
*/
'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 +
')' + '<br />';
//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.closed) {
var shape = new google.maps.Polygon({
'name': name,
'clickable': clickable,
'geodesic': false,
'map': null,
'strokeColor': region.color,
'strokeOpacity': region.opacity,
'strokeWeight': overviewerConfig.CONST.regionStrokeWeight,
'fillColor': region.color,
'fillOpacity': region.opacity * 0.25,
'zIndex': j,
'paths': converted
});
} else {
var shape = new google.maps.Polyline({
'name': name,
'clickable': clickable,
'geodesic': false,
'map': null,
'strokeColor': region.color,
'strokeOpacity': region.opacity,
'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.label);
}
});
}
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.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.label, item.checked);
}
dropdownDiv.appendChild(itemDiv);
itemDiv.appendChild(itemInput);
var textNode = document.createElement('text');
if(item.icon) {
textNode.innerHTML = '<img width="15" height="15" src="' +
item.icon + '">' + item.label + '<br/>';
} else {
textNode.innerHTML = item.label + '<br/>';
}
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 = '<b>Region: ' + shape.name + '</b><br />' +
'Clicked Location: <br />' + event.latLng.lat() + ', ' +
event.latLng.lng() + '<br />';
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 = '<div class="infoWindow"><img src="' + marker.icon +
'"/><p>' + marker.title.replace(/\n/g,'<br/>') + '</p></div>';
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);
}
}
}
};

408
world.py
View File

@@ -16,6 +16,7 @@
import functools
import os
import os.path
from glob import glob
import multiprocessing
import Queue
import sys
@@ -29,46 +30,16 @@ import numpy
import chunk
import nbt
import textures
import time
"""
This module has routines related to generating all the chunks for a world
and for extracting information about available worlds
This module has routines for extracting information about available worlds
"""
base36decode = functools.partial(int, base=36)
cached = collections.defaultdict(dict)
def _convert_coords(chunks):
"""Takes the list of (chunkx, chunky, chunkfile) where chunkx and chunky
are in the chunk coordinate system, and figures out the row and column in
the image each one should be.
returns mincol, maxcol, minrow, maxrow, chunks_translated
chunks_translated is a list of (col, row, (chunkX, chunkY))
The (chunkX, chunkY) tuple is the chunkCoords, used to identify the
chunk file
"""
chunks_translated = []
# columns are determined by the sum of the chunk coords, rows are the
# difference
item = chunks[0]
mincol = maxcol = item[0] + item[1]
minrow = maxrow = item[1] - item[0]
for c in chunks:
col = c[0] + c[1]
mincol = min(mincol, col)
maxcol = max(maxcol, col)
row = c[1] - c[0]
minrow = min(minrow, row)
maxrow = max(maxrow, row)
chunks_translated.append((col, row, (c[0],c[1])))
return mincol, maxcol, minrow, maxrow, chunks_translated
def base36encode(number, alphabet='0123456789abcdefghijklmnopqrstuvwxyz'):
'''
Convert an integer to a base36 string.
@@ -91,33 +62,37 @@ def base36encode(number, alphabet='0123456789abcdefghijklmnopqrstuvwxyz'):
return "-" + base36
return base36
class FakeAsyncResult:
def __init__(self, string):
self.string = string
def get(self):
return self.string
class WorldRenderer(object):
"""Renders a world's worth of chunks.
class World(object):
"""Does world-level preprocessing to prepare for QuadtreeGen
worlddir is the path to the minecraft world
cachedir is the path to a directory that should hold the resulting images.
It may be the same as worlddir (which used to be the default).
If chunklist is given, it is assumed to be an iterator over paths to chunk
files to update. If it includes a trailing newline, it is stripped, so you
can pass in file handles just fine.
"""
def __init__(self, worlddir, cachedir, chunklist=None, lighting=False, night=False, spawn=False, useBiomeData=False):
self.worlddir = worlddir
self.caves = False
self.lighting = lighting or night or spawn
self.night = night or spawn
self.spawn = spawn
self.cachedir = cachedir
self.useBiomeData = useBiomeData
mincol = maxcol = minrow = maxrow = 0
def __init__(self, worlddir, useBiomeData=False,regionlist=None):
self.worlddir = worlddir
self.useBiomeData = useBiomeData
#find region files, or load the region list
#this also caches all the region file header info
logging.info("Scanning regions")
regionfiles = {}
self.regions = {}
for x, y, regionfile in self._iterate_regionfiles():
mcr = self.reload_region(regionfile)
mcr.get_chunk_info()
regionfiles[(x,y)] = (x,y,regionfile,mcr)
self.regionfiles = regionfiles
# set the number of region file handles we will permit open at any time before we start closing them
# self.regionlimit = 1000
# the max number of chunks we will keep before removing them (includes emptry chunks)
self.chunklimit = 1024
self.chunkcount = 0
self.empty_chunk = [None,None]
logging.debug("Done scanning regions")
# figure out chunk format is in use
# if mcregion, error out early until we can add support
# if not mcregion, error out early
data = nbt.load(os.path.join(self.worlddir, "level.dat"))[1]['Data']
#print data
if not ('version' in data and data['version'] == 19132):
@@ -127,22 +102,6 @@ class WorldRenderer(object):
if self.useBiomeData:
textures.prepareBiomeData(worlddir)
self.chunklist = chunklist
# In order to avoid having to look up the cache file names in
# ChunkRenderer, get them all and store them here
# TODO change how caching works
for root, dirnames, filenames in os.walk(cachedir):
for filename in filenames:
if not filename.endswith('.png') or not filename.startswith("img."):
continue
dirname, dir_b = os.path.split(root)
_, dir_a = os.path.split(dirname)
_, x, z, cave, _ = filename.split('.', 4)
dir = '/'.join((dir_a, dir_b))
bits = '.'.join((x, z, cave))
cached[dir][bits] = os.path.join(root, filename)
# stores Points Of Interest to be mapped with markers
# a list of dictionaries, see below for an example
self.POI = []
@@ -151,57 +110,88 @@ class WorldRenderer(object):
# info self.persistentData. This dictionary can hold any information
# that may be needed between runs.
# Currently only holds into about POIs (more more details, see quadtree)
self.pickleFile = os.path.join(self.cachedir,"overviewer.dat")
# TODO maybe store this with the tiles, not with the world?
self.pickleFile = os.path.join(self.worlddir, "overviewer.dat")
if os.path.exists(self.pickleFile):
with open(self.pickleFile,"rb") as p:
self.persistentData = cPickle.load(p)
else:
# some defaults
self.persistentData = dict(POI=[])
def _get_chunk_renderset(self):
"""Returns a set of (col, row) chunks that should be rendered. Returns
None if all chunks should be rendered"""
if not self.chunklist:
return None
raise Exception("not yet working") ## TODO correctly reimplement this for mcregion
# Get a list of the (chunks, chunky, filename) from the passed in list
# of filenames
chunklist = []
for path in self.chunklist:
if path.endswith("\n"):
path = path[:-1]
f = os.path.basename(path)
if f and f.startswith("c.") and f.endswith(".dat"):
p = f.split(".")
chunklist.append((base36decode(p[1]), base36decode(p[2]),
path))
if not chunklist:
logging.error("No valid chunks specified in your chunklist!")
logging.error("HINT: chunks are in your world directory and have names of the form 'c.*.*.dat'")
sys.exit(1)
# Translate to col, row coordinates
_, _, _, _, chunklist = _convert_coords(chunklist)
# Build a set from the col, row pairs
inclusion_set = set()
for col, row, filename in chunklist:
inclusion_set.add((col, row))
return inclusion_set
def get_region_path(self, chunkX, chunkY):
"""Returns the path to the region that contains chunk (chunkX, chunkY)
"""
chunkFile = "region/r.%s.%s.mcr" % (chunkX//32, chunkY//32)
_, _, regionfile,_ = self.regionfiles.get((chunkX//32, chunkY//32),(None,None,None,None));
return regionfile
def load_from_region(self,filename, x, y):
#we need to manage the chunk cache
regioninfo = self.regions[filename]
if regioninfo is None:
return None
chunks = regioninfo[2]
chunk_data = chunks.get((x,y))
if chunk_data is None:
#prune the cache if required
if self.chunkcount > self.chunklimit: #todo: make the emptying the chunk cache slightly less crazy
[self.reload_region(regionfile) for regionfile in self.regions if regionfile <> filename]
self.chunkcount = 0
self.chunkcount += 1
return os.path.join(self.worlddir, chunkFile)
nbt = self.load_region(filename).load_chunk(x, y)
if nbt is None:
chunks[(x,y)] = self.empty_chunk
return None ## return none. I think this is who we should indicate missing chunks
#raise IOError("No such chunk in region: (%i, %i)" % (x, y))
#we cache the transformed data, not it's raw form
data = nbt.read_all()
level = data[1]['Level']
chunk_data = level
#chunk_data = {}
#chunk_data['skylight'] = chunk.get_skylight_array(level)
#chunk_data['blocklight'] = chunk.get_blocklight_array(level)
#chunk_data['blockarray'] = chunk.get_blockdata_array(level)
#chunk_data['TileEntities'] = chunk.get_tileentity_data(level)
chunks[(x,y)] = [level,time.time()]
else:
chunk_data = chunk_data[0]
return chunk_data
#used to reload a changed region
def reload_region(self,filename):
if self.regions.get(filename) is not None:
self.regions[filename][0].closefile()
chunkcache = {}
mcr = nbt.MCRFileReader(filename)
self.regions[filename] = (mcr,os.path.getmtime(filename),chunkcache)
return mcr
def load_region(self,filename):
return self.regions[filename][0]
def get_region_mtime(self,filename):
return (self.regions[filename][0],self.regions[filename][1])
def convert_coords(self, chunkx, chunky):
"""Takes a coordinate (chunkx, chunky) where chunkx and chunky are
in the chunk coordinate system, and figures out the row and column
in the image each one should be. Returns (col, row)."""
# columns are determined by the sum of the chunk coords, rows are the
# difference (TODO: be able to change direction of north)
# change this function, and you MUST change unconvert_coords
return (chunkx + chunky, chunky - chunkx)
def unconvert_coords(self, col, row):
"""Undoes what convert_coords does. Returns (chunkx, chunky)."""
# col + row = chunky + chunky => (col + row)/2 = chunky
# col - row = chunkx + chunkx => (col - row)/2 = chunkx
return ((col - row) / 2, (col + row) / 2)
def findTrueSpawn(self):
"""Adds the true spawn location to self.POI. The spawn Y coordinate
@@ -237,22 +227,44 @@ class WorldRenderer(object):
self.POI.append( dict(x=spawnX, y=spawnY, z=spawnZ,
msg="Spawn", type="spawn", chunk=(inChunkX,inChunkZ)))
self.spawn = (spawnX, spawnY, spawnZ)
def go(self, procs):
"""Starts the render. This returns when it is finished"""
"""Scan the world directory, to fill in
self.{min,max}{col,row} for use later in quadtree.py. This
also does other world-level processing."""
logging.info("Scanning chunks")
raw_chunks = self._get_chunklist()
# find the dimensions of the map, in region files
minx = maxx = miny = maxy = 0
found_regions = False
for x, y in self.regionfiles:
found_regions = True
minx = min(minx, x)
maxx = max(maxx, x)
miny = min(miny, y)
maxy = max(maxy, y)
if not found_regions:
logging.error("Error: No chunks found!")
sys.exit(1)
logging.debug("Done scanning chunks")
# turn our region coordinates into chunk coordinates
minx = minx * 32
miny = miny * 32
maxx = maxx * 32 + 32
maxy = maxy * 32 + 32
# Translate chunks to our diagonal coordinate system
# TODO
mincol, maxcol, minrow, maxrow, chunks = _convert_coords(raw_chunks)
del raw_chunks # Free some memory
self.chunkmap = self._render_chunks_async(chunks, procs)
logging.debug("world chunkmap has len %d", len(self.chunkmap))
mincol = maxcol = minrow = maxrow = 0
for chunkx, chunky in [(minx, miny), (minx, maxy), (maxx, miny), (maxx, maxy)]:
col, row = self.convert_coords(chunkx, chunky)
mincol = min(mincol, col)
maxcol = max(maxcol, col)
minrow = min(minrow, row)
maxrow = max(maxrow, row)
#logging.debug("map size: (%i, %i) to (%i, %i)" % (mincol, minrow, maxcol, maxrow))
self.mincol = mincol
self.maxcol = maxcol
@@ -261,147 +273,25 @@ class WorldRenderer(object):
self.findTrueSpawn()
def _find_regionfiles(self):
"""Returns a list of all of the region files, along with their
def _iterate_regionfiles(self,regionlist=None):
"""Returns an iterator of all of the region files, along with their
coordinates
Returns (regionx, regiony, filename)"""
all_chunks = []
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(".")
all_chunks.append((int(p[1]), int(p[2]),
os.path.join(dirpath, f)))
return all_chunks
def _get_chunklist(self):
"""Returns a list of all possible chunk coordinates, based on the
available regions files. Note that not all chunk coordinates will
exists. The chunkrender will know how to ignore non-existant chunks
returns a list of (chunkx, chunky, regionfile) where regionfile is
the region file that contains this chunk
TODO, a --cachedir implemetation should involved thie method
"""
all_chunks = []
regions = self._find_regionfiles()
logging.debug("Found %d regions",len(regions))
for region in regions:
these_chunks = list(itertools.product(
range(region[0]*32,region[0]*32 + 32),
range(region[1]*32,region[1]*32 + 32)
))
these_chunks = map(lambda x: (x[0], x[1], region[2]), these_chunks)
assert(len(these_chunks) == 1024)
all_chunks += these_chunks
if not all_chunks:
logging.error("Error: No chunks found!")
sys.exit(1)
logging.debug("Total possible chunks: %d", len(all_chunks))
return all_chunks
def _render_chunks_async(self, chunks, processes):
"""Starts up a process pool and renders all the chunks asynchronously.
chunks is a list of (col, row, (chunkX, chunkY)). Use chunkX,chunkY
to find the chunk data in a region file
Returns a dictionary mapping (col, row) to the file where that
chunk is rendered as an image
"""
# The set of chunks to render, or None for all of them. The logic is
# slightly more compliated than it should seem, since we still need to
# build the results dict out of all chunks, even if they're not being
# rendered.
inclusion_set = self._get_chunk_renderset()
results = {}
manager = multiprocessing.Manager()
q = manager.Queue()
if processes == 1:
# Skip the multiprocessing stuff
logging.debug("Rendering chunks synchronously since you requested 1 process")
for i, (col, row, chunkXY) in enumerate(chunks):
##TODO##/if inclusion_set and (col, row) not in inclusion_set:
##TODO##/ # Skip rendering, just find where the existing image is
##TODO##/ _, imgpath = chunk.find_oldimage(chunkfile, cached, self.caves)
##TODO##/ if imgpath:
##TODO##/ results[(col, row)] = imgpath
##TODO##/ continue
oldimg = chunk.find_oldimage(chunkXY, cached, self.caves)
# TODO remove this shortcircuit
if chunk.check_cache(self, chunkXY, oldimg):
result = oldimg[1]
else:
#logging.debug("check cache failed, need to render (could be ghost chunk)")
result = chunk.render_and_save(chunkXY, self.cachedir, self, oldimg, queue=q)
if result:
results[(col, row)] = result
if i > 0:
try:
item = q.get(block=False)
if item[0] == "newpoi":
self.POI.append(item[1])
elif item[0] == "removePOI":
self.persistentData['POI'] = filter(lambda x: x['chunk'] != item[1], self.persistentData['POI'])
except Queue.Empty:
pass
if 1000 % i == 0 or i % 1000 == 0:
logging.info("{0}/{1} chunks rendered".format(i, len(chunks)))
else:
logging.debug("Rendering chunks in {0} processes".format(processes))
pool = multiprocessing.Pool(processes=processes)
asyncresults = []
for col, row, chunkXY in chunks:
##TODO/if inclusion_set and (col, row) not in inclusion_set:
##TODO/ # Skip rendering, just find where the existing image is
##TODO/ _, imgpath = chunk.find_oldimage(chunkfile, cached, self.caves)
##TODO/ if imgpath:
##TODO/ results[(col, row)] = imgpath
##TODO/ continue
oldimg = chunk.find_oldimage(chunkXY, cached, self.caves)
if chunk.check_cache(self, chunkXY, oldimg):
result = FakeAsyncResult(oldimg[1])
else:
result = pool.apply_async(chunk.render_and_save,
args=(chunkXY,self.cachedir,self, oldimg),
kwds=dict(cave=self.caves, queue=q))
asyncresults.append((col, row, result))
pool.close()
for i, (col, row, result) in enumerate(asyncresults):
results[(col, row)] = result.get()
try:
item = q.get(block=False)
if item[0] == "newpoi":
self.POI.append(item[1])
elif item[0] == "removePOI":
self.persistentData['POI'] = filter(lambda x: x['chunk'] != item[1], self.persistentData['POI'])
except Queue.Empty:
pass
if i > 0:
if 1000 % i == 0 or i % 1000 == 0:
logging.info("{0}/{1} chunks rendered".format(i, len(asyncresults)))
pool.join()
logging.info("Done!")
return results
join = os.path.join
if regionlist is not None:
for path in regionlist:
if path.endswith("\n"):
path = path[:-1]
f = os.path.basename(path)
if f.startswith("r.") and f.endswith(".mcr"):
p = f.split(".")
yield (int(p[1]), int(p[2]), join(self.worlddir, 'region', f))
else:
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