Merge remote-tracking branch 'upstream/master'
This commit is contained in:
16
.gitignore
vendored
16
.gitignore
vendored
@@ -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
53
CONTRIBUTORS.rst
Normal 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>
|
||||
188
README.rst
188
README.rst
@@ -1,21 +1,26 @@
|
||||
====================
|
||||
Minecraft Overviewer
|
||||
====================
|
||||
By Andrew Brown and contributors
|
||||
By Andrew Brown and contributors (see CONTRIBUTORS.rst).
|
||||
|
||||
http://github.com/brownan/Minecraft-Overviewer
|
||||
|
||||
Generates large resolution images of a Minecraft map.
|
||||
|
||||
In short, this program reads in Minecraft world files and renders very large
|
||||
resolution images. It performs a similar function to the existing Minecraft
|
||||
Cartographer program but with a slightly different goal in mind: to generate
|
||||
large resolution images such that one can zoom in and see details.
|
||||
resolution images that can be viewed through a Google Maps interface. It
|
||||
performs a similar function to the existing Minecraft Cartographer program but
|
||||
with a slightly different goal in mind: to generate large resolution images
|
||||
such that one can zoom in and see details.
|
||||
|
||||
See some examples here!
|
||||
http://github.com/brownan/Minecraft-Overviewer/wiki/Map-examples
|
||||
|
||||
(To contact me, send me a message on Github)
|
||||
Further documentation may be found at
|
||||
https://github.com/brownan/Minecraft-Overviewer/wiki/Documentation
|
||||
|
||||
To contact the developers and other users, go to the site at the top of this
|
||||
README, or go to #overviewer on irc.freenode.net.
|
||||
|
||||
Features
|
||||
========
|
||||
@@ -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,62 +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.
|
||||
This function should accept one argument: a QuadtreeGen object.
|
||||
|
||||
Viewing the Results
|
||||
-------------------
|
||||
@@ -299,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.
|
||||
|
||||
224
_composite.c
224
_composite.c
@@ -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);
|
||||
}
|
||||
590
chunk.py
590
chunk.py
@@ -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,18 +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):
|
||||
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:
|
||||
logging.warning("Error opening chunk (%i, %i) in %s. It may be corrupt. %s", x, y, filename, e)
|
||||
raise ChunkCorrupt(str(e))
|
||||
if retries > 0:
|
||||
# wait a little bit, and try again (up to `retries` times)
|
||||
time.sleep(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
|
||||
@@ -65,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):
|
||||
@@ -103,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])
|
||||
@@ -118,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
|
||||
|
||||
@@ -189,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
|
||||
|
||||
@@ -210,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
|
||||
@@ -286,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)
|
||||
@@ -320,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)
|
||||
@@ -354,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)
|
||||
@@ -369,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)
|
||||
@@ -390,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
|
||||
@@ -429,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
|
||||
@@ -451,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,
|
||||
@@ -634,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
|
||||
@@ -657,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
|
||||
@@ -676,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']])
|
||||
@@ -889,7 +424,8 @@ 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.
|
||||
@@ -928,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():
|
||||
@@ -937,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:
|
||||
|
||||
19
composite.py
19
composite.py
@@ -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)
|
||||
|
||||
56
config.js
56
config.js
@@ -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;
|
||||
@@ -1,8 +1,11 @@
|
||||
from optparse import OptionParser
|
||||
import sys
|
||||
import os.path
|
||||
import logging
|
||||
|
||||
class OptionsResults(object):
|
||||
pass
|
||||
def get(self, *args):
|
||||
return self.__dict__.get(*args)
|
||||
|
||||
class ConfigOptionParser(object):
|
||||
def __init__(self, **kwargs):
|
||||
@@ -12,17 +15,21 @@ class ConfigOptionParser(object):
|
||||
|
||||
# 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"]
|
||||
|
||||
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):
|
||||
@@ -34,6 +41,8 @@ class ConfigOptionParser(object):
|
||||
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):
|
||||
@@ -61,18 +70,19 @@ class ConfigOptionParser(object):
|
||||
g['args'] = args
|
||||
|
||||
try:
|
||||
execfile(self.configFile, g, l)
|
||||
if os.path.exists(self.configFile):
|
||||
execfile(self.configFile, g, l)
|
||||
except NameError, ex:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
print "\nError parsing %s. Please check the trackback above" % self.configFile
|
||||
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
|
||||
print "\nError parsing %s. Please check the trackback above" % self.configFile
|
||||
logging.error("Error parsing %s. Please check the trackback above" % self.configFile)
|
||||
sys.exit(1)
|
||||
|
||||
#print l.keys()
|
||||
@@ -83,7 +93,7 @@ class ConfigOptionParser(object):
|
||||
n = a['dest']
|
||||
if a.get('commandLineOnly', False):
|
||||
if n in l.keys():
|
||||
print "Error: %s can only be specified on the command line. It is not valid in the config file" % n
|
||||
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)
|
||||
@@ -107,33 +117,23 @@ class ConfigOptionParser(object):
|
||||
for a in self.configVars:
|
||||
n = a['dest']
|
||||
if configResults.__dict__[n] == None and a.get('required',False):
|
||||
raise Exception("%s is required" % n)
|
||||
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:
|
||||
# switch on type. there are only 6 types that can be used with optparse
|
||||
if a['type'] == "int":
|
||||
configResults.__dict__[n] = int(configResults.__dict__[n])
|
||||
elif a['type'] == "string":
|
||||
configResults.__dict__[n] = str(configResults.__dict__[n])
|
||||
elif a['type'] == "long":
|
||||
configResults.__dict__[n] = long(configResults.__dict__[n])
|
||||
elif a['type'] == "choice":
|
||||
if configResults.__dict__[n] not in a['choices']:
|
||||
print "The value '%s' is not valid for config parameter '%s'" % (configResults.__dict__[n], n)
|
||||
sys.exit(1)
|
||||
elif a['type'] == "float":
|
||||
configResults.__dict__[n] = long(configResults.__dict__[n])
|
||||
elif a['type'] == "complex":
|
||||
configResults.__dict__[n] = complex(configResults.__dict__[n])
|
||||
else:
|
||||
print "Unknown type!"
|
||||
sys.exit(1)
|
||||
configResults.__dict__[n] = self.checkType(configResults.__dict__[n], a)
|
||||
except ValueError, ex:
|
||||
print "There was a problem converting the value '%s' to type %s for config parameter '%s'" % (configResults.__dict__[n], a['type'], n)
|
||||
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)
|
||||
@@ -144,3 +144,30 @@ class ConfigOptionParser(object):
|
||||
|
||||
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)
|
||||
|
||||
94
contrib/clearOldCache.py
Normal file
94
contrib/clearOldCache.py
Normal 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()
|
||||
@@ -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'])
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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
156
contrib/testRender.py
Normal 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()
|
||||
105
contrib/validateRegionFile.py
Normal file
105
contrib/validateRegionFile.py
Normal 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
176
googlemap.py
Normal 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
182
nbt.py
@@ -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)
|
||||
|
||||
@@ -23,11 +23,14 @@ advdef = "advdef"
|
||||
|
||||
def check_programs(level):
|
||||
path = os.environ.get("PATH").split(os.pathsep)
|
||||
|
||||
|
||||
def exists_in_path(prog):
|
||||
result = filter(lambda x: os.path.exists(os.path.join(x, prog)), path)
|
||||
return len(result) != 0
|
||||
|
||||
for prog,l in [(pngcrush,1), (optipng,2), (advdef,2)]:
|
||||
if l <= level:
|
||||
result = filter(lambda x: os.path.exists(os.path.join(x, prog)), path)
|
||||
if len(result) == 0:
|
||||
if (not exists_in_path(prog)) and (not exists_in_path(prog + ".exe")):
|
||||
raise Exception("Optimization prog %s for level %d not found!" % (prog, l))
|
||||
|
||||
def optimize_image(imgpath, imgformat, optimizeimg):
|
||||
|
||||
@@ -24,35 +24,82 @@ import os
|
||||
import os.path
|
||||
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
|
||||
|
||||
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", 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("--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("--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("--rendermode", dest="rendermode", help="Specifies the render type: normal (default), lighting, night, or spawn.", type="choice", choices=["normal", "lighting", "night", "spawn"], required=True, default="normal")
|
||||
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")
|
||||
@@ -61,8 +108,28 @@ def main():
|
||||
|
||||
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)
|
||||
@@ -75,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:
|
||||
@@ -88,22 +155,17 @@ 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]
|
||||
@@ -114,7 +176,7 @@ def main():
|
||||
|
||||
|
||||
if options.delete:
|
||||
return delete_all(cachedir, destdir)
|
||||
return delete_all(worlddir, destdir)
|
||||
|
||||
if options.chunklist:
|
||||
chunklist = open(options.chunklist, 'r')
|
||||
@@ -134,7 +196,7 @@ def main():
|
||||
optimizeimages.check_programs(optimizeimg)
|
||||
else:
|
||||
optimizeimg = None
|
||||
|
||||
|
||||
logging.getLogger().setLevel(
|
||||
logging.getLogger().level + 10*options.quiet)
|
||||
logging.getLogger().setLevel(
|
||||
@@ -142,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, rendermode=options.rendermode, 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)
|
||||
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
157
overviewerConfig.js
Normal 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 https://github.com/pironic/WG2OvR 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}
|
||||
};
|
||||
755
quadtree.py
755
quadtree.py
@@ -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):
|
||||
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,11 +62,20 @@ class QuadtreeGen(object):
|
||||
assert(imgformat)
|
||||
self.imgformat = imgformat
|
||||
self.optimizeimg = optimizeimg
|
||||
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):
|
||||
@@ -130,85 +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:
|
||||
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('];')
|
||||
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
|
||||
@@ -216,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)
|
||||
@@ -230,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
|
||||
@@ -244,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):
|
||||
@@ -257,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
|
||||
@@ -284,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:
|
||||
@@ -337,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
|
||||
@@ -417,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
366
rendernode.py
Normal 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
149
sample.settings.py
Normal 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
|
||||
|
||||
|
||||
77
setup.py
77
setup.py
@@ -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,19 +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
|
||||
#
|
||||
|
||||
setup_kwargs['ext_modules'].append(Extension('_composite', ['_composite.c'], include_dirs=['.'], extra_link_args=["/MANIFEST"] if platform.system() == "Windows" else []))
|
||||
# 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 = []
|
||||
|
||||
# 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}
|
||||
@@ -51,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:
|
||||
@@ -64,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
902
src/Draw.c
Normal 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
360
src/composite.c
Normal 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
39
src/endian.c
Normal 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
379
src/iterate.c
Normal 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
64
src/main.c
Normal 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
91
src/overviewer.h
Normal 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
234
src/rendermode-cave.c
Normal 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
238
src/rendermode-lighting.c
Normal 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
70
src/rendermode-night.c
Normal 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
230
src/rendermode-normal.c
Normal 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
137
src/rendermode-overlay.c
Normal 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
118
src/rendermode-spawn.c
Normal 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
184
src/rendermodes.c
Normal 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
159
src/rendermodes.h
Normal 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__ */
|
||||
1006
textures.py
1006
textures.py
File diff suppressed because it is too large
Load Diff
21
util.py
21
util.py
@@ -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"
|
||||
|
||||
@@ -1,455 +0,0 @@
|
||||
var prevInfoWindow = null;
|
||||
|
||||
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
|
||||
});
|
||||
google.maps.event.addListener(marker, 'click', function() {
|
||||
if (prevInfoWindow)
|
||||
prevInfoWindow.close()
|
||||
infowindow.open(map,marker);
|
||||
prevInfoWindow = infowindow
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
function drawMapControls() {
|
||||
|
||||
// viewstate link
|
||||
var viewStateDiv = document.createElement('DIV');
|
||||
|
||||
//<div id="link" style="border:1px solid black;background-color:white;color:black;position:absolute;top:5px;right:5px"></div>
|
||||
|
||||
viewStateDiv.id="link";
|
||||
|
||||
|
||||
map.controls[google.maps.ControlPosition.BOTTOM_RIGHT].push(viewStateDiv);
|
||||
|
||||
|
||||
// compass rose, in the top right corner
|
||||
var compassDiv = document.createElement('DIV');
|
||||
|
||||
compassDiv.style.padding = '5px';
|
||||
|
||||
var compassImg = document.createElement('IMG');
|
||||
compassImg.src="compass.png";
|
||||
compassDiv.appendChild(compassImg);
|
||||
|
||||
compassDiv.index = 0;
|
||||
map.controls[google.maps.ControlPosition.TOP_RIGHT].push(compassDiv);
|
||||
|
||||
|
||||
if (signGroups.length > 0) {
|
||||
// signpost display control
|
||||
//
|
||||
|
||||
var signControl = document.createElement("DIV");
|
||||
signControl.id = "signControl"; // let's let a style sheet do most of the styling here
|
||||
|
||||
var controlBorder = document.createElement("DIV");
|
||||
controlBorder.id="top";
|
||||
signControl.appendChild(controlBorder);
|
||||
|
||||
var controlText = document.createElement("DIV");
|
||||
|
||||
controlBorder.appendChild(controlText);
|
||||
|
||||
controlText.innerHTML = "Signposts";
|
||||
|
||||
var dropdownDiv = document.createElement("DIV");
|
||||
|
||||
|
||||
$(controlText).click(function() {
|
||||
$(dropdownDiv).toggle();
|
||||
|
||||
});
|
||||
|
||||
|
||||
dropdownDiv.id="dropDown";
|
||||
signControl.appendChild(dropdownDiv);
|
||||
dropdownDiv.innerHTML="";
|
||||
|
||||
map.controls[google.maps.ControlPosition.TOP_RIGHT].push(signControl);
|
||||
|
||||
|
||||
|
||||
var hasSignGroup = false;
|
||||
for (idx in signGroups) {
|
||||
var item = signGroups[idx];
|
||||
//console.log(item);
|
||||
label = item.label;
|
||||
hasSignGroup = true;
|
||||
var d = document.createElement("div");
|
||||
var n = document.createElement("input");
|
||||
n.type="checkbox";
|
||||
|
||||
$(n).data("label",label);
|
||||
jQuery(n).click(function(e) {
|
||||
var t = $(e.target);
|
||||
jQuery.each(markerCollection[t.data("label")], function(i,elem) {elem.setVisible(e.target.checked);});
|
||||
});
|
||||
|
||||
|
||||
if (item.checked) {
|
||||
n.checked = true;
|
||||
jQuery.each(markerCollection[label], function(i,elem) {elem.setVisible(n.checked);});
|
||||
}
|
||||
dropdownDiv.appendChild(d);
|
||||
d.appendChild(n)
|
||||
var textNode = document.createElement("text");
|
||||
textNode.innerHTML = label + "<br/>";
|
||||
d.appendChild(textNode);
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
function initRegions() {
|
||||
if (regionsInit) { return; }
|
||||
|
||||
regionsInit = true;
|
||||
|
||||
for (i in regionData) {
|
||||
var region = regionData[i];
|
||||
var converted = new google.maps.MVCArray();
|
||||
for (j in region.path) {
|
||||
var point = region.path[j];
|
||||
converted.push(fromWorldToLatLng(point.x, point.y, point.z));
|
||||
}
|
||||
|
||||
if (region.closed) {
|
||||
new google.maps.Polygon({clickable: false,
|
||||
geodesic: false,
|
||||
map: map,
|
||||
strokeColor: region.color,
|
||||
strokeOpacity: region.opacity,
|
||||
strokeWeight: 2,
|
||||
fillColor: region.color,
|
||||
fillOpacity: region.opacity * 0.25,
|
||||
zIndex: i,
|
||||
paths: converted
|
||||
});
|
||||
} else {
|
||||
new google.maps.Polyline({clickable: false,
|
||||
geodesic: false,
|
||||
map: map,
|
||||
strokeColor: region.color,
|
||||
strokeOpacity: region.opacity,
|
||||
strokeWeight: 2,
|
||||
zIndex: i,
|
||||
path: converted
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
function initMarkers() {
|
||||
if (markersInit) { return; }
|
||||
|
||||
markersInit = true;
|
||||
|
||||
for (i in markerData) {
|
||||
var item = markerData[i];
|
||||
|
||||
// a default:
|
||||
var iconURL = '';
|
||||
if (item.type == 'spawn') {
|
||||
// don't filter spawn, always display
|
||||
|
||||
iconURL = 'http://google-maps-icons.googlecode.com/files/home.png';
|
||||
var converted = fromWorldToLatLng(item.x, item.y, item.z);
|
||||
var marker = new google.maps.Marker({position: converted,
|
||||
map: map,
|
||||
title: jQuery.trim(item.msg),
|
||||
icon: iconURL
|
||||
});
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
var matched = false;
|
||||
for (idx in signGroups) {
|
||||
var signGroup = signGroups[idx];
|
||||
var testfunc = signGroup.match;
|
||||
var label = signGroup.label;
|
||||
|
||||
if (testfunc(item)) {
|
||||
matched = true;
|
||||
|
||||
if (item.type == 'sign') { iconURL = 'signpost_icon.png';}
|
||||
|
||||
//console.log(signGroup.icon);
|
||||
if (signGroup.icon) { iconURL = signGroup.icon;
|
||||
}
|
||||
|
||||
var converted = fromWorldToLatLng(item.x, item.y, item.z);
|
||||
var marker = new google.maps.Marker({position: converted,
|
||||
map: map,
|
||||
title: jQuery.trim(item.msg),
|
||||
icon: iconURL,
|
||||
visible: false
|
||||
});
|
||||
if (markerCollection[label]) {
|
||||
markerCollection[label].push(marker);
|
||||
} else {
|
||||
markerCollection[label] = [marker];
|
||||
}
|
||||
|
||||
if (item.type == 'sign') {
|
||||
prepareSignMarker(marker, item);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
if (!matched) {
|
||||
// is this signpost doesn't match any of the groups in config.js, add it automatically to the "__others__" group
|
||||
if (item.type == 'sign') { iconURL = 'signpost_icon.png';}
|
||||
|
||||
var converted = fromWorldToLatLng(item.x, item.y, item.z);
|
||||
var marker = new google.maps.Marker({position: converted,
|
||||
map: map,
|
||||
title: jQuery.trim(item.msg),
|
||||
icon: iconURL,
|
||||
visible: false
|
||||
});
|
||||
if (markerCollection["__others__"]) {
|
||||
markerCollection["__others__"].push(marker);
|
||||
} else {
|
||||
markerCollection["__others__"] = [marker];
|
||||
}
|
||||
|
||||
if (item.type == 'sign') {
|
||||
prepareSignMarker(marker, item);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function makeLink() {
|
||||
var a=location.href.substring(0,location.href.lastIndexOf(location.search))
|
||||
+ "?lat=" + map.getCenter().lat().toFixed(6)
|
||||
+ "&lng=" + map.getCenter().lng().toFixed(6)
|
||||
+ "&zoom=" + map.getZoom();
|
||||
document.getElementById("link").innerHTML = a;
|
||||
}
|
||||
|
||||
function initialize() {
|
||||
|
||||
var query = location.search.substring(1);
|
||||
|
||||
var lat = 0.5;
|
||||
var lng = 0.5;
|
||||
var zoom = config.defaultZoom;
|
||||
var pairs = query.split("&");
|
||||
for (var i=0; i<pairs.length; i++) {
|
||||
// break each pair at the first "=" to obtain the argname and value
|
||||
var pos = pairs[i].indexOf("=");
|
||||
var argname = pairs[i].substring(0,pos).toLowerCase();
|
||||
var value = pairs[i].substring(pos+1).toLowerCase();
|
||||
|
||||
// process each possible argname
|
||||
if (argname == "lat") {lat = parseFloat(value);}
|
||||
if (argname == "lng") {lng = parseFloat(value);}
|
||||
if (argname == "zoom") {zoom = parseInt(value);}
|
||||
}
|
||||
|
||||
var mapTyepControlToggle = false
|
||||
if (mapTypeIds.length > 1) {
|
||||
mapTyepControlToggle = true
|
||||
}
|
||||
var mapOptions = {
|
||||
zoom: zoom,
|
||||
center: new google.maps.LatLng(lat, lng),
|
||||
navigationControl: true,
|
||||
scaleControl: false,
|
||||
mapTypeControl: mapTyepControlToggle,
|
||||
mapTypeControlOptions: {
|
||||
mapTypeIds: mapTypeIds
|
||||
},
|
||||
mapTypeId: mapTypeIdDefault,
|
||||
streetViewControl: false,
|
||||
backgroundColor: config.bg_color,
|
||||
};
|
||||
map = new google.maps.Map(document.getElementById('mcmap'), mapOptions);
|
||||
|
||||
if(config.debug) {
|
||||
map.overlayMapTypes.insertAt(0, new CoordMapType(new google.maps.Size(config.tileSize, config.tileSize)));
|
||||
|
||||
google.maps.event.addListener(map, 'click', function(event) {
|
||||
//console.log("latLng; " + event.latLng.lat() + ", " + event.latLng.lng());
|
||||
|
||||
var pnt = map.getProjection().fromLatLngToPoint(event.latLng);
|
||||
//console.log("point: " + pnt);
|
||||
|
||||
var pxx = pnt.x * config.tileSize * Math.pow(2, config.maxZoom);
|
||||
var pxy = pnt.y * config.tileSize * Math.pow(2, config.maxZoom);
|
||||
//console.log("pixel: " + pxx + ", " + pxy);
|
||||
});
|
||||
}
|
||||
|
||||
// Now attach the coordinate map type to the map's registry
|
||||
for (idx in MCMapType) {
|
||||
map.mapTypes.set('mcmap' + MCMapType[idx].name, MCMapType[idx]);
|
||||
}
|
||||
|
||||
// We can now set the map to use the 'coordinate' map type
|
||||
map.setMapTypeId(mapTypeIdDefault);
|
||||
|
||||
// initialize the markers and regions
|
||||
initMarkers();
|
||||
initRegions();
|
||||
drawMapControls();
|
||||
|
||||
//makeLink();
|
||||
|
||||
// Make the link again whenever the map changes
|
||||
google.maps.event.addListener(map, 'zoom_changed', function() {
|
||||
makeLink();
|
||||
});
|
||||
google.maps.event.addListener(map, 'center_changed', function() {
|
||||
makeLink();
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
// our custom projection maps Latitude to Y, and Longitude to X as normal,
|
||||
// but it maps the range [0.0, 1.0] to [0, tileSize] in both directions
|
||||
// so it is easier to position markers, etc. based on their position
|
||||
// (find their position in the lowest-zoom image, and divide by tileSize)
|
||||
function MCMapProjection() {
|
||||
this.inverseTileSize = 1.0 / config.tileSize;
|
||||
}
|
||||
|
||||
MCMapProjection.prototype.fromLatLngToPoint = function(latLng) {
|
||||
var x = latLng.lng() * config.tileSize;
|
||||
var y = latLng.lat() * config.tileSize;
|
||||
return new google.maps.Point(x, y);
|
||||
};
|
||||
|
||||
MCMapProjection.prototype.fromPointToLatLng = function(point) {
|
||||
var lng = point.x * this.inverseTileSize;
|
||||
var lat = point.y * this.inverseTileSize;
|
||||
return new google.maps.LatLng(lat, lng);
|
||||
};
|
||||
|
||||
// helper to get map LatLng from world coordinates
|
||||
// takes arguments in X, Y, Z order
|
||||
// (arguments are *out of order*, because within the function we use
|
||||
// the axes like the rest of Minecraft Overviewer -- with the Z and Y
|
||||
// flipped from normal minecraft usage.)
|
||||
function fromWorldToLatLng(x, z, y)
|
||||
{
|
||||
// the width and height of all the highest-zoom tiles combined, inverted
|
||||
var perPixel = 1.0 / (config.tileSize * Math.pow(2, config.maxZoom));
|
||||
|
||||
// This information about where the center column is may change with a different
|
||||
// drawing implementation -- check it again after any drawing overhauls!
|
||||
|
||||
// point (0, 0, 127) is at (0.5, 0.0) of tile (tiles/2 - 1, tiles/2)
|
||||
// so the Y coordinate is at 0.5, and the X is at 0.5 - ((tileSize / 2) / (tileSize * 2^maxZoom))
|
||||
// or equivalently, 0.5 - (1 / 2^(maxZoom + 1))
|
||||
var lng = 0.5 - (1.0 / Math.pow(2, config.maxZoom + 1));
|
||||
var lat = 0.5;
|
||||
|
||||
// the following metrics mimic those in ChunkRenderer.chunk_render in "chunk.py"
|
||||
|
||||
// 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 and 18px to the Y to center our point
|
||||
lng += 12 * perPixel;
|
||||
lat += 18 * perPixel;
|
||||
|
||||
return new google.maps.LatLng(lat, lng);
|
||||
}
|
||||
|
||||
function getTileUrlGenerator(path, path_base) {
|
||||
return function(tile, zoom) {
|
||||
var url = path;
|
||||
var url_base = ( path_base ? path_base : '' );
|
||||
if(tile.x < 0 || tile.x >= Math.pow(2, zoom) || tile.y < 0 || tile.y >= Math.pow(2, zoom)) {
|
||||
url += '/blank';
|
||||
} else if(zoom == 0) {
|
||||
url += '/base';
|
||||
} else {
|
||||
for(var z = zoom - 1; z >= 0; --z) {
|
||||
var x = Math.floor(tile.x / Math.pow(2, z)) % 2;
|
||||
var y = Math.floor(tile.y / Math.pow(2, z)) % 2;
|
||||
url += '/' + (x + 2 * y);
|
||||
}
|
||||
}
|
||||
url = url + '.' + config.fileExt;
|
||||
if(config.cacheMinutes > 0) {
|
||||
var d = new Date();
|
||||
url += '?c=' + Math.floor(d.getTime() / (1000 * 60 * config.cacheMinutes));
|
||||
}
|
||||
return(url_base + url);
|
||||
}
|
||||
}
|
||||
|
||||
var MCMapOptions = new Array;
|
||||
var MCMapType = new Array;
|
||||
var mapTypeIdDefault = null;
|
||||
var mapTypeIds = [];
|
||||
for (idx in mapTypeData) {
|
||||
var view = mapTypeData[idx];
|
||||
|
||||
MCMapOptions[view.label] = {
|
||||
getTileUrl: getTileUrlGenerator(view.path, view.base),
|
||||
tileSize: new google.maps.Size(config.tileSize, config.tileSize),
|
||||
maxZoom: config.maxZoom,
|
||||
minZoom: 0,
|
||||
isPng: !(config.fileExt.match(/^png$/i) == null)
|
||||
};
|
||||
|
||||
MCMapType[view.label] = new google.maps.ImageMapType(MCMapOptions[view.label]);
|
||||
MCMapType[view.label].name = view.label;
|
||||
MCMapType[view.label].alt = "Minecraft " + view.label + " Map";
|
||||
MCMapType[view.label].projection = new MCMapProjection();
|
||||
if (mapTypeIdDefault == null) {
|
||||
mapTypeIdDefault = 'mcmap' + view.label;
|
||||
}
|
||||
mapTypeIds.push('mcmap' + view.label);
|
||||
}
|
||||
|
||||
function CoordMapType() {
|
||||
}
|
||||
|
||||
function CoordMapType(tileSize) {
|
||||
this.tileSize = tileSize;
|
||||
}
|
||||
|
||||
CoordMapType.prototype.getTile = function(coord, zoom, ownerDocument) {
|
||||
var div = ownerDocument.createElement('DIV');
|
||||
div.innerHTML = "(" + coord.x + ", " + coord.y + ", " + zoom + ")";
|
||||
div.innerHTML += "<br />";
|
||||
div.innerHTML += MCMapOptions.getTileUrl(coord, zoom);
|
||||
div.style.width = this.tileSize.width + 'px';
|
||||
div.style.height = this.tileSize.height + 'px';
|
||||
div.style.fontSize = '10';
|
||||
div.style.borderStyle = 'solid';
|
||||
div.style.borderWidth = '1px';
|
||||
div.style.borderColor = '#AAAAAA';
|
||||
return div;
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
@@ -1,6 +1,18 @@
|
||||
html { height: 100% }
|
||||
body { height: 100%; margin: 0px; padding: 0px ; background-color: #000; }
|
||||
#mcmap { height: 100% }
|
||||
html {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
height: 100%;
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
#mcmap {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.infoWindow {
|
||||
height: 100px;
|
||||
@@ -17,13 +29,13 @@ body { height: 100%; margin: 0px; padding: 0px ; background-color: #000; }
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
#signControl {
|
||||
#customControl {
|
||||
padding: 5px;
|
||||
height: 15px;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
|
||||
#signControl > div#top {
|
||||
#customControl > div#top {
|
||||
background-color: #fff;
|
||||
border: 2px solid #000;
|
||||
text-align: center;
|
||||
@@ -33,7 +45,14 @@ body { height: 100%; margin: 0px; padding: 0px ; background-color: #000; }
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#signControl > div#dropDown {
|
||||
#customControl > div#dropDown {
|
||||
border: 1px solid #000;
|
||||
font-size: 12px;
|
||||
background-color: #fff;
|
||||
display: none;
|
||||
}
|
||||
|
||||
#customControl > div#button {
|
||||
border: 1px solid #000;
|
||||
font-size: 12px;
|
||||
background-color: #fff;
|
||||
893
web_assets/overviewer.js
Normal file
893
web_assets/overviewer.js
Normal 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
408
world.py
@@ -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, rendermode="normal", useBiomeData=False):
|
||||
self.worlddir = worlddir
|
||||
self.caves = False
|
||||
self.lighting = rendermode in ("lighting","night","spawn")
|
||||
self.night = rendermode in ("night","spawn")
|
||||
self.spawn = rendermode in ("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
|
||||
|
||||
Reference in New Issue
Block a user