0

Merge branch 'master' into snapshot

This commit is contained in:
Andrew Chin
2014-08-26 11:55:00 -04:00
21 changed files with 871 additions and 348 deletions

View File

@@ -20,7 +20,7 @@ The Minecraft Overviewer is a command-line tool for rendering high-resolution
maps of Minecraft worlds. It generates a set of static html and image files and maps of Minecraft worlds. It generates a set of static html and image files and
uses the Google Maps API to display a nice interactive map. uses the Google Maps API to display a nice interactive map.
The Overviewer has been in active development for over a year and has many The Overviewer has been in active development for several years and has many
features, including day and night lighting, cave rendering, mineral overlays, features, including day and night lighting, cave rendering, mineral overlays,
and many plugins for even more features! It is written mostly in Python with and many plugins for even more features! It is written mostly in Python with
critical sections in C as an extension module. critical sections in C as an extension module.

View File

@@ -41,16 +41,16 @@ master_doc = 'index'
# General information about the project. # General information about the project.
project = u'Overviewer' project = u'Overviewer'
copyright = u'2010-2012 The Overviewer Team' copyright = u'2010-2014 The Overviewer Team'
# The version info for the project you're documenting, acts as replacement for # The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the # |version| and |release|, also used in various other places throughout the
# built documents. # built documents.
# #
# The short X.Y version. # The short X.Y version.
version = "0.10" version = "0.11"
# The full version, including alpha/beta/rc tags. # The full version, including alpha/beta/rc tags.
release = "0.10" release = "0.11"
# The language for content autogenerated by Sphinx. Refer to documentation # The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages. # for a list of supported languages.

View File

@@ -19,8 +19,14 @@ Python, don't worry, it's pretty simple. Just follow the examples.
Windows. This is required because the backslash ("\\") has special meaning Windows. This is required because the backslash ("\\") has special meaning
in Python. in Python.
Examples
========
The following examples should give you an idea of what a configuration file looks
like, and also teach you some neat tricks.
A Simple Example A Simple Example
================ ----------------
:: ::
@@ -60,7 +66,7 @@ The ``renders`` dictionary
``worlds["My world"]`` ``worlds["My world"]``
A more complicated example A more complicated example
========================== --------------------------
:: ::
worlds["survival"] = "/home/username/server/survivalworld" worlds["survival"] = "/home/username/server/survivalworld"
@@ -130,7 +136,7 @@ renders.
example. example.
A dynamic config file A dynamic config file
===================== ---------------------
It might be handy to dynamically retrieve parameters. For instance, if you It might be handy to dynamically retrieve parameters. For instance, if you
periodically render your last map backup which is located in a timestamped periodically render your last map backup which is located in a timestamped
@@ -192,6 +198,9 @@ If the above doesn't make sense, just know that items in the config file take
the form ``key = value``. Two items take a different form:, ``worlds`` and the form ``key = value``. Two items take a different form:, ``worlds`` and
``renders``, which are described below. ``renders``, which are described below.
General
-------
``worlds`` ``worlds``
This is pre-defined as an empty dictionary. The config file is expected to This is pre-defined as an empty dictionary. The config file is expected to
add at least one item to it. add at least one item to it.
@@ -259,6 +268,9 @@ the form ``key = value``. Two items take a different form:, ``worlds`` and
processes = 2 processes = 2
Observers
~~~~~~~~~
.. _observer: .. _observer:
``observer = <observer object>`` ``observer = <observer object>``
@@ -367,6 +379,8 @@ the form ``key = value``. Two items take a different form:, ``worlds`` and
Custom web assets
~~~~~~~~~~~~~~~~~
.. _customwebassets: .. _customwebassets:
@@ -411,6 +425,9 @@ values. The valid configuration keys are listed below.
'title': 'This render doesn't explicitly declare a world!', 'title': 'This render doesn't explicitly declare a world!',
} }
General
~~~~~~~
``world`` ``world``
Specifies which world this render corresponds to. Its value should be a Specifies which world this render corresponds to. Its value should be a
string from the appropriate key in the worlds dictionary. string from the appropriate key in the worlds dictionary.
@@ -442,8 +459,21 @@ values. The valid configuration keys are listed below.
nether :ref:`rendermode<option_rendermode>`. Otherwise you'll nether :ref:`rendermode<option_rendermode>`. Otherwise you'll
just end up rendering the nether's ceiling. just end up rendering the nether's ceiling.
.. note::
For the end, you will most likely want to turn down the strength of
the shadows, as you'd otherwise end up with a very dark result.
e.g.::
end_lighting = [Base(), EdgeLines(), Lighting(strength=0.5)]
end_smooth_lighting = [Base(), EdgeLines(), SmoothLighting(strength=0.5)]
**Default:** ``"overworld"`` **Default:** ``"overworld"``
Rendering
~~~~~~~~~
.. _option_rendermode: .. _option_rendermode:
``rendermode`` ``rendermode``
@@ -523,215 +553,6 @@ values. The valid configuration keys are listed below.
**Default:** ``"upper-left"`` **Default:** ``"upper-left"``
.. _rerenderprob:
``rerenderprob``
This is the probability that a tile will be rerendered even though there may
have been no changes to any blocks within that tile. Its value should be a
floating point number between 0.0 and 1.0.
**Default:** ``0``
``imgformat``
This is which image format to render the tiles into. Its value should be a
string containing "png", "jpg", or "jpeg".
**Default:** ``"png"``
``imgquality``
This is the image quality used when saving the tiles into the JPEG image
format. Its value should be an integer between 0 and 100.
**Default:** ``95``
``optimizeimg``
This option specifies which additional tools overviewer should use to
optimize the filesize of png tiles.
The tools used must be placed somewhere, where overviewer can find them, for
example the "PATH" environment variable or a directory like /usr/bin.
This should be an integer between 0 and 3.
* ``1 - Use pngcrush``
* ``2 - Use advdef``
* ``3 - Use pngcrush and advdef (Not recommended)``
Using this option may significantly increase render time, but will make
the resulting tiles smaller, with lossless image quality.
**Default:** ``0``
``bgcolor``
This is the background color to be displayed behind the map. Its value
should be either a string in the standard HTML color syntax or a 4-tuple in
the format of (r,b,g,a). The alpha entry should be set to 0.
**Default:** ``#1a1a1a``
``defaultzoom``
This value specifies the default zoom level that the map will be opened
with. It has to be greater than 0.
**Default:** ``1``
``maxzoom``
This specifies the maximum zoom allowed by the zoom control on the web page.
.. note::
This does not change the number of zoom levels rendered, but allows
you to neglect uploading the larger and more detailed zoom levels if bandwidth
usage is an issue.
**Default:** Automatically set to most detailed zoom level
``minzoom``
This specifies the minimum zoom allowed by the zoom control on the web page. For
example, setting this to 2 will disable the two most-zoomed out levels.
.. note::
This does not change the number of zoom levels rendered, but allows
you to have control over the number of zoom levels accessible via the
slider control.
**Default:** 0 (zero, which does not disable any zoom levels)
``showlocationmarker``
Allows you to specify whether to show the location marker when accessing a URL
with coordinates specified.
**Default:** ``True``
``base``
Allows you to specify a remote location for the tile folder, useful if you
rsync your map's images to a remote server. Leave a trailing slash and point
to the location that contains the tile folders for each render, not the
tiles folder itself. For example, if the tile images start at
http://domain.com/map/world_day/ you want to set this to http://domain.com/map/
.. _option_texturepath:
``texturepath``
This is a where a specific texture or resource pack can be found to use
during this render. It can be a path to either a folder or a zip/jar file
containing the texture resources. If specifying a folder, this option should
point to a directory that *contains* the assets/ directory (it should not
point to the assets directory directly or any one particular texture image).
Its value should be a string: the path on the filesystem to the resource
pack.
.. _crop:
``crop``
You can use this to render a small subset of your map, instead of the entire
thing. The format is (min x, min z, max x, max z).
The coordinates are block coordinates. The same you get with the debug menu
in-game and the coordinates shown when you view a map.
Example that only renders a 1000 by 1000 square of land about the origin::
renders['myrender'] = {
'world': 'myworld',
'title': "Cropped Example",
'crop': (-500, -500, 500, 500),
}
This option performs a similar function to the old ``--regionlist`` option
(which no longer exists). It is useful for example if someone has wandered
really far off and made your map too large. You can set the crop for the
largest map you want to render (perhaps ``(-10000,-10000,10000,10000)``). It
could also be used to define a really small render showing off one
particular feature, perhaps from multiple angles.
.. warning::
If you decide to change the bounds on a render, you may find it produces
unexpected results. It is recommended to not change the crop settings
once it has been rendered once.
For an expansion to the bounds, because chunks in the new bounds have
the same mtime as the old, tiles will not automatically be updated,
leaving strange artifacts along the old border. You may need to use
:option:`--forcerender` to force those tiles to update. (You can use
the ``forcerender`` option on just one render by adding ``'forcerender':
True`` to that render's configuration)
For reductions to the bounds, you will need to render your map at least
once with the :option:`--check-tiles` mode activated, and then once with
the :option:`--forcerender` option. The first run will go and delete tiles that
should no longer exist, while the second will render the tiles around
the edge properly. Also see :ref:`this faq entry<cropping_faq>`.
Sorry there's no better way to handle these cases at the moment. It's a
tricky problem and nobody has devoted the effort to solve it yet.
``forcerender``
This is a boolean. If set to ``True`` (or any non-false value) then this
render will unconditionally re-render every tile regardless of whether it
actually needs updating or not.
The :option:`--forcerender` command line option acts similarly, but with
one important difference. Say you have 3 renders defined in your
configuration file. If you use :option:`--forcerender`, then all 3 of those
renders get re-rendered completely. However, if you just need one of them
re-rendered, that's unnecessary extra work.
If you set ``'forcerender': True,`` on just one of those renders, then just
that one gets re-rendered completely. The other two render normally (only
tiles that need updating are rendered).
You probably don't want to leave this option in your config file, it is
intended to be used temporarily, such as after a setting change, to
re-render the entire map with new settings. If you leave it in, then
Overviewer will end up doing a lot of unnecessary work rendering parts of
your map that may not have changed.
Example::
renders['myrender'] = {
'world': 'myworld',
'title': "Forced Example",
'forcerender': True,
}
``changelist``
This is a string. It names a file where it will write out, one per line, the
path to tiles that have been updated. You can specify the same file for
multiple (or all) renders and they will all be written to the same file. The
file is cleared when The Overviewer starts.
This option is useful in conjunction with a simple upload script, to upload
the files that have changed.
.. warning::
A solution like ``rsync -a --delete`` is much better because it also
watches for tiles that should be *deleted*, which is impossible to
convey with the changelist option. If your map ever shrinks or you've
removed some tiles, you may need to do some manual deletion on the
remote side.
.. _option_markers:
``markers``
This controls the display of markers, signs, and other points of interest
in the output HTML. It should be a list of dictionaries.
.. note::
Setting this configuration option alone does nothing. In order to get
markers and signs on our map, you must also run the genPO script. See
the :doc:`Signs and markers<signs>` section for more details and documenation.
**Default:** ``[]`` (an empty list)
``poititle``
This controls the display name of the POI/marker dropdown control.
**Default:** "Signs"
.. _option_overlay: .. _option_overlay:
``overlay`` ``overlay``
@@ -773,10 +594,338 @@ values. The valid configuration keys are listed below.
**Default:** ``[]`` (an empty list) **Default:** ``[]`` (an empty list)
.. _option_texturepath:
``texturepath``
This is a where a specific texture or resource pack can be found to use
during this render. It can be a path to either a folder or a zip/jar file
containing the texture resources. If specifying a folder, this option should
point to a directory that *contains* the assets/ directory (it should not
point to the assets directory directly or any one particular texture image).
Its value should be a string: the path on the filesystem to the resource
pack.
.. _crop:
``crop``
You can use this to render one or more small subsets of your map. The format
of an individual crop zone is (min x, min z, max x, max z); if you wish to
specify multiple crop zones, you may do so by specifying a list of crop zones,
i.e. [(min x1, min z1, max x1, max z1), (min x2, min z2, max x2, max z2)]
The coordinates are block coordinates. The same you get with the debug menu
in-game and the coordinates shown when you view a map.
Example that only renders a 1000 by 1000 square of land about the origin::
renders['myrender'] = {
'world': 'myworld',
'title': "Cropped Example",
'crop': (-500, -500, 500, 500),
}
Example that renders two 500 by 500 squares of land::
renders['myrender'] = {
'world': 'myworld',
'title': "Multi cropped Example",
'crop': [(-500, -500, 0, 0), (0, 0, 500, 500)]
}
This option performs a similar function to the old ``--regionlist`` option
(which no longer exists). It is useful for example if someone has wandered
really far off and made your map too large. You can set the crop for the
largest map you want to render (perhaps ``(-10000,-10000,10000,10000)``). It
could also be used to define a really small render showing off one
particular feature, perhaps from multiple angles.
.. warning::
If you decide to change the bounds on a render, you may find it produces
unexpected results. It is recommended to not change the crop settings
once it has been rendered once.
For an expansion to the bounds, because chunks in the new bounds have
the same mtime as the old, tiles will not automatically be updated,
leaving strange artifacts along the old border. You may need to use
:option:`--forcerender` to force those tiles to update. (You can use
the ``forcerender`` option on just one render by adding ``'forcerender':
True`` to that render's configuration)
For reductions to the bounds, you will need to render your map at least
once with the :option:`--check-tiles` mode activated, and then once with
the :option:`--forcerender` option. The first run will go and delete tiles that
should no longer exist, while the second will render the tiles around
the edge properly. Also see :ref:`this faq entry<cropping_faq>`.
Sorry there's no better way to handle these cases at the moment. It's a
tricky problem and nobody has devoted the effort to solve it yet.
Image options
~~~~~~~~~~~~~
``imgformat``
This is which image format to render the tiles into. Its value should be a
string containing "png", "jpg", or "jpeg".
**Default:** ``"png"``
``imgquality``
This is the image quality used when saving the tiles into the JPEG image
format. Its value should be an integer between 0 and 100.
**Default:** ``95``
``optimizeimg``
.. warning::
Using image optimizers will increase render times significantly.
This option specifies which additional tools overviewer should use to
optimize the filesize of png tiles.
The tools used must be placed somewhere, where overviewer can find them, for
example the "PATH" environment variable or a directory like /usr/bin.
The option is a list of Optimizer objects, which are then executed in
the order in which they're specified::
# Import the optimizers we need
from optimizeimages import pngnq, optipng
worlds["world"] = "/path/to/world"
renders["daytime"] = {
"world":"world",
"title":"day",
"rendermode":smooth_lighting,
"optimizeimg":[pngnq(sampling=1), optipng(olevel=3)],
}
.. note::
Don't forget to import the optimizers you use in your config file, as shown in the
example above.
Here is a list of supported image optimization programs:
``pngnq``
pngnq quantizes 32-bit RGBA images into 8-bit RGBA palette PNGs. This is
lossy, but reduces filesize significantly. Available settings:
``sampling``
An integer between ``1`` and ``10``, ``1`` samples all pixels, is slow and yields
the best quality. Higher values sample less of the image, which makes
the process faster, but less accurate.
**Default:** ``3``
``dither``
Either the string ``"n"`` for no dithering, or ``"f"`` for Floyd
Steinberg dithering. Dithering helps eliminate colorbanding, sometimes
increasing visual quality.
.. warning::
With pngnq version 1.0 (which is what Ubuntu 12.04 ships), the
dithering option is broken. Only the default, no dithering,
can be specified on those systems.
**Default:** ``"n"``
.. warning::
Because of several PIL bugs, only the most zoomed in level has transparency
when using pngnq. The other zoom levels have all transparency replaced by
black. This is *not* pngnq's fault, as pngnq supports multiple levels of
transparency just fine, it's PIL's fault for not even reading indexed
PNGs correctly.
``optipng``
optipng tunes the deflate algorithm and removes unneeded channels from the PNG,
producing a smaller, lossless output image. It was inspired by pngcrush.
Available settings:
``olevel``
An integer between ``0`` (few optimizations) and ``7`` (many optimizations).
The default should be satisfactory for everyone, higher levels than the default
see almost no benefit.
**Default:** ``2``
``pngcrush``
pngcrush, like optipng, is a lossless PNG recompressor. If you are able to do so, it
is recommended to use optipng instead, as it generally yields better results in less
time.
Available settings:
``brute``
Either ``True`` or ``False``. Cycles through all compression methods, and is very slow.
.. note::
There is practically no reason to ever use this. optipng will beat pngcrush, and
throwing more CPU time at pngcrush most likely won't help. If you think you need
this option, then you are most likely wrong.
**Default:** ``False``
**Default:** ``[]``
Zoom
~~~~
These options control the zooming behavior in the JavaScript output.
``defaultzoom``
This value specifies the default zoom level that the map will be
opened with. It has to be greater than 0, which corresponds to the
most zoomed-out level. If you use ``minzoom`` or ``maxzoom``, it
should be between those two.
**Default:** ``1``
``maxzoom``
This specifies the maximum, closest in zoom allowed by the zoom
control on the web page. This is relative to 0, the farthest-out
image, so setting this to 8 will allow you to zoom in at most 8
times. This is *not* relative to ``minzoom``, so setting
``minzoom`` will shave off even more levels. If you wish to
specify how many zoom levels to leave off, instead of how many
total to use, use a negative number here. For example, setting
this to -2 will disable the two most zoomed-in levels.
.. note::
This does not change the number of zoom levels rendered, but allows
you to neglect uploading the larger and more detailed zoom levels if bandwidth
usage is an issue.
**Default:** Automatically set to most detailed zoom level
``minzoom``
This specifies the minimum, farthest away zoom allowed by the zoom
control on the web page. For example, setting this to 2 will
disable the two most zoomed-out levels.
.. note::
This does not change the number of zoom levels rendered, but allows
you to have control over the number of zoom levels accessible via the
slider control.
**Default:** 0 (zero, which does not disable any zoom levels)
Other HTML/JS output options
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
``showlocationmarker``
Allows you to specify whether to show the location marker when accessing a URL
with coordinates specified.
**Default:** ``True``
``base``
Allows you to specify a remote location for the tile folder, useful if you
rsync your map's images to a remote server. Leave a trailing slash and point
to the location that contains the tile folders for each render, not the
tiles folder itself. For example, if the tile images start at
http://domain.com/map/world_day/ you want to set this to http://domain.com/map/
.. _option_markers:
``markers``
This controls the display of markers, signs, and other points of interest
in the output HTML. It should be a list of dictionaries.
.. note::
Setting this configuration option alone does nothing. In order to get
markers and signs on our map, you must also run the genPO script. See
the :doc:`Signs and markers<signs>` section for more details and documenation.
**Default:** ``[]`` (an empty list)
``poititle``
This controls the display name of the POI/marker dropdown control.
**Default:** "Signs"
``showspawn`` ``showspawn``
This is a boolean, and defaults to ``True``. If set to ``False``, then the spawn This is a boolean, and defaults to ``True``. If set to ``False``, then the spawn
icon will not be displayed on the rendered map. icon will not be displayed on the rendered map.
``bgcolor``
This is the background color to be displayed behind the map. Its value
should be either a string in the standard HTML color syntax or a 4-tuple in
the format of (r,b,g,a). The alpha entry should be set to 0.
**Default:** ``#1a1a1a``
Map update behavior
~~~~~~~~~~~~~~~~~~~
.. _rerenderprob:
``rerenderprob``
This is the probability that a tile will be rerendered even though there may
have been no changes to any blocks within that tile. Its value should be a
floating point number between 0.0 and 1.0.
**Default:** ``0``
``forcerender``
This is a boolean. If set to ``True`` (or any non-false value) then this
render will unconditionally re-render every tile regardless of whether it
actually needs updating or not.
The :option:`--forcerender` command line option acts similarly, but with
one important difference. Say you have 3 renders defined in your
configuration file. If you use :option:`--forcerender`, then all 3 of those
renders get re-rendered completely. However, if you just need one of them
re-rendered, that's unnecessary extra work.
If you set ``'forcerender': True,`` on just one of those renders, then just
that one gets re-rendered completely. The other two render normally (only
tiles that need updating are rendered).
You probably don't want to leave this option in your config file, it is
intended to be used temporarily, such as after a setting change, to
re-render the entire map with new settings. If you leave it in, then
Overviewer will end up doing a lot of unnecessary work rendering parts of
your map that may not have changed.
Example::
renders['myrender'] = {
'world': 'myworld',
'title': "Forced Example",
'forcerender': True,
}
``renderchecks``
This is an integer, and functions as a more complex form of
``forcerender``. Setting it to 1 enables :option:`--check-tiles`
mode, setting it to 2 enables :option:`--forcerender`, and 3 tells
Overviewer to keep this particular render in the output, but
otherwise don't update it. It defaults to 0, which is the usual
update checking mode.
``changelist``
This is a string. It names a file where it will write out, one per line, the
path to tiles that have been updated. You can specify the same file for
multiple (or all) renders and they will all be written to the same file. The
file is cleared when The Overviewer starts.
This option is useful in conjunction with a simple upload script, to upload
the files that have changed.
.. warning::
A solution like ``rsync -a --delete`` is much better because it also
watches for tiles that should be *deleted*, which is impossible to
convey with the changelist option. If your map ever shrinks or you've
removed some tiles, you may need to do some manual deletion on the
remote side.
.. _customrendermodes: .. _customrendermodes:
Custom Rendermodes and Rendermode Primitives Custom Rendermodes and Rendermode Primitives
@@ -967,6 +1116,7 @@ BiomeOverlay
Defining Custom Rendermodes Defining Custom Rendermodes
--------------------------- ---------------------------
Each rendermode primitive listed above is a Python *class* that is automatically Each rendermode primitive listed above is a Python *class* that is automatically
imported in the context of the config file (They come from imported in the context of the config file (They come from
overviewer_core.rendermodes). To define your own rendermode, simply define a overviewer_core.rendermodes). To define your own rendermode, simply define a
@@ -994,7 +1144,8 @@ are referencing the previously defined list, not one of the built-in
rendermodes. rendermodes.
Built-in Rendermodes Built-in Rendermodes
==================== --------------------
The built-in rendermodes are nothing but pre-defined lists of rendermode The built-in rendermodes are nothing but pre-defined lists of rendermode
primitives for your convenience. Here are their definitions:: primitives for your convenience. Here are their definitions::

View File

@@ -34,7 +34,7 @@ hereby called "blocks", where each block in the world's grid has a type that
determines what it is (grass, stone, ...). This makes worlds relatively determines what it is (grass, stone, ...). This makes worlds relatively
uncomplicated to render, the Overviewer simply determines what blocks to draw uncomplicated to render, the Overviewer simply determines what blocks to draw
and where. Since everything in Minecraft is aligned to a strict grid, placement and where. Since everything in Minecraft is aligned to a strict grid, placement
and rendering decisions are completely deterministic and can be performed in an and rendering decisions are completely deterministic and can be performed
iteratively. iteratively.
The coordinate system for Minecraft has three axes. The X and Z axes are the The coordinate system for Minecraft has three axes. The X and Z axes are the

View File

@@ -8,6 +8,22 @@ Frequently Asked Questions
General Questions General Questions
================= =================
Does the Overviewer work with mod blocks?
-----------------------------------------
The Overviewer will render the world, but none of the blocks added by mods
will be visible. Currently, the blocks Overviewer supports are hardcoded, and
because there is no official Minecraft modding API as of the time of writing,
supporting mod blocks is not trivial.
Can I view Overviewer maps without having an internet connection?
-----------------------------------------------------------------
Not at the moment. The Overviewer relies on the Google maps API to display
maps, which your browser needs to load from Google. However, switching away
from Google Maps is something that will most likely be looked into in the
future.
When my map expands, I see remnants of another zoom level When my map expands, I see remnants of another zoom level
--------------------------------------------------------- ---------------------------------------------------------

View File

@@ -18,7 +18,7 @@ The Minecraft Overviewer is a command-line tool for rendering high-resolution
maps of Minecraft worlds. It generates a set of static html and image files and maps of Minecraft worlds. It generates a set of static html and image files and
uses the Google Maps API to display a nice interactive map. uses the Google Maps API to display a nice interactive map.
The Overviewer has been in active development for over a year and has many The Overviewer has been in active development for several years and has many
features, including day and night lighting, cave rendering, mineral overlays, features, including day and night lighting, cave rendering, mineral overlays,
and many plugins for even more features! It is written mostly in Python with and many plugins for even more features! It is written mostly in Python with
critical sections in C as an extension module. critical sections in C as an extension module.
@@ -114,7 +114,7 @@ Windows, Mac, and Linux as long as you have these software packages installed:
* Python 2.6 or 2.7 (we are *not* yet compatible with Python 3.x) * Python 2.6 or 2.7 (we are *not* yet compatible with Python 3.x)
* PIL (Python Imaging Library) * PIL (Python Imaging Library) or Pillow
* Numpy * Numpy

View File

@@ -261,13 +261,13 @@ If you want or need to provide your own textures, you have several options:
:: ::
VERSION=1.7.2 VERSION=1.7.10
wget https://s3.amazonaws.com/Minecraft.Download/versions/${VERSION}/${VERSION}.jar -P ~/.minecraft/versions/${VERSION}/ wget https://s3.amazonaws.com/Minecraft.Download/versions/${VERSION}/${VERSION}.jar -P ~/.minecraft/versions/${VERSION}/
If that's too confusing for you, then just take this single line and paste it into If that's too confusing for you, then just take this single line and paste it into
a terminal to get 1.7.2 textures:: a terminal to get 1.7.10 textures::
wget https://s3.amazonaws.com/Minecraft.Download/versions/1.7.2/1.7.2.jar -P ~/.minecraft/versions/1.7.2/ wget https://s3.amazonaws.com/Minecraft.Download/versions/1.7.10/1.7.10.jar -P ~/.minecraft/versions/1.7.10/
* You can also just run the launcher to install the client. * You can also just run the launcher to install the client.

View File

@@ -84,6 +84,8 @@ def main():
help="Tries to locate the texture files. Useful for debugging texture problems.") help="Tries to locate the texture files. Useful for debugging texture problems.")
parser.add_option("-V", "--version", dest="version", parser.add_option("-V", "--version", dest="version",
help="Displays version information and then exits", action="store_true") help="Displays version information and then exits", action="store_true")
parser.add_option("--check-version", dest="checkversion",
help="Fetchs information about the latest version of Overviewer", action="store_true")
parser.add_option("--update-web-assets", dest='update_web_assets', action="store_true", parser.add_option("--update-web-assets", dest='update_web_assets', action="store_true",
help="Update web assets. Will *not* render tiles or update overviewerConfig.js") help="Update web assets. Will *not* render tiles or update overviewerConfig.js")
@@ -141,8 +143,28 @@ def main():
if options.verbose > 0: if options.verbose > 0:
print("Python executable: %r" % sys.executable) print("Python executable: %r" % sys.executable)
print(sys.version) print(sys.version)
if not options.checkversion:
return 0
if options.checkversion:
print("Currently running Minecraft Overviewer %s" % util.findGitVersion()),
print("(%s)" % util.findGitHash()[:7])
try:
import urllib
import json
latest_ver = json.loads(urllib.urlopen("http://overviewer.org/download.json").read())['src']
print("Latest version of Minecraft Overviewer %s (%s)" % (latest_ver['version'], latest_ver['commit'][:7]))
print("See http://overviewer.org/downloads for more information")
except Exception:
print("Failed to fetch latest version info.")
if options.verbose > 0:
import traceback
traceback.print_exc()
else:
print("Re-run with --verbose for more details")
return 1
return 0 return 0
if options.pid: if options.pid:
if os.path.exists(options.pid): if os.path.exists(options.pid):
try: try:
@@ -318,19 +340,24 @@ dir but you forgot to put quotes around the directory, since it contains spaces.
"--check-tiles, and --no-tile-checks. These options conflict.") "--check-tiles, and --no-tile-checks. These options conflict.")
parser.print_help() parser.print_help()
return 1 return 1
def set_renderchecks(checkname, num):
for name, render in config['renders'].iteritems():
if render.get('renderchecks', 0) == 3:
logging.warning(checkname + " ignoring render " + repr(name) + " since it's marked as \"don't render\".")
else:
render['renderchecks'] = num
if options.forcerender: if options.forcerender:
logging.info("Forcerender mode activated. ALL tiles will be rendered") logging.info("Forcerender mode activated. ALL tiles will be rendered")
for render in config['renders'].itervalues(): set_renderchecks("forcerender", 2)
render['renderchecks'] = 2
elif options.checktiles: elif options.checktiles:
logging.info("Checking all tiles for updates manually.") logging.info("Checking all tiles for updates manually.")
for render in config['renders'].itervalues(): set_renderchecks("checktiles", 1)
render['renderchecks'] = 1
elif options.notilechecks: elif options.notilechecks:
logging.info("Disabling all tile mtime checks. Only rendering tiles "+ logging.info("Disabling all tile mtime checks. Only rendering tiles "+
"that need updating since last render") "that need updating since last render")
for render in config['renders'].itervalues(): set_renderchecks("notilechecks", 0)
render['renderchecks'] = 0
if not config['renders']: if not config['renders']:
logging.error("You must specify at least one render in your config file. See the docs if you're having trouble") logging.error("You must specify at least one render in your config file. See the docs if you're having trouble")
@@ -461,16 +488,20 @@ dir but you forgot to put quotes around the directory, since it contains spaces.
# regionset cache pulls from the same underlying cache object. # regionset cache pulls from the same underlying cache object.
rset = world.CachedRegionSet(rset, caches) rset = world.CachedRegionSet(rset, caches)
# If a crop is requested, wrap the regionset here
if "crop" in render:
rset = world.CroppedRegionSet(rset, *render['crop'])
# If this is to be a rotated regionset, wrap it in a RotatedRegionSet # If this is to be a rotated regionset, wrap it in a RotatedRegionSet
# object # object
if (render['northdirection'] > 0): if (render['northdirection'] > 0):
rset = world.RotatedRegionSet(rset, render['northdirection']) rset = world.RotatedRegionSet(rset, render['northdirection'])
logging.debug("Using RegionSet %r", rset) logging.debug("Using RegionSet %r", rset)
# If a crop is requested, wrap the regionset here
if "crop" in render:
rsets = []
for zone in render['crop']:
rsets.append(world.CroppedRegionSet(rset, *zone))
else:
rsets = [rset]
############################### ###############################
# Do the final prep and create the TileSet object # Do the final prep and create the TileSet object
@@ -481,8 +512,9 @@ dir but you forgot to put quotes around the directory, since it contains spaces.
render['name'] = render_name # perhaps a hack. This is stored here for the asset manager render['name'] = render_name # perhaps a hack. This is stored here for the asset manager
tileSetOpts = util.dict_subset(render, ["name", "imgformat", "renderchecks", "rerenderprob", "bgcolor", "defaultzoom", "imgquality", "optimizeimg", "rendermode", "worldname_orig", "title", "dimension", "changelist", "showspawn", "overlay", "base", "poititle", "maxzoom", "showlocationmarker", "minzoom"]) tileSetOpts = util.dict_subset(render, ["name", "imgformat", "renderchecks", "rerenderprob", "bgcolor", "defaultzoom", "imgquality", "optimizeimg", "rendermode", "worldname_orig", "title", "dimension", "changelist", "showspawn", "overlay", "base", "poititle", "maxzoom", "showlocationmarker", "minzoom"])
tileSetOpts.update({"spawn": w.find_true_spawn()}) # TODO find a better way to do this tileSetOpts.update({"spawn": w.find_true_spawn()}) # TODO find a better way to do this
tset = tileset.TileSet(w, rset, assetMrg, tex, tileSetOpts, tileset_dir) for rset in rsets:
tilesets.append(tset) tset = tileset.TileSet(w, rset, assetMrg, tex, tileSetOpts, tileset_dir)
tilesets.append(tset)
# Do tileset preprocessing here, before we start dispatching jobs # Do tileset preprocessing here, before we start dispatching jobs
logging.info("Preprocessing...") logging.info("Preprocessing...")
@@ -514,6 +546,8 @@ dir but you forgot to put quotes around the directory, since it contains spaces.
if options.pid: if options.pid:
os.remove(options.pid) os.remove(options.pid)
logging.info("Your render has been written to '%s', open index.html to view it" % destdir)
return 0 return 0
def list_worlds(): def list_worlds():

View File

@@ -15,12 +15,15 @@ markers.js holds a list of which markerSets are attached to each tileSet
''' '''
import os import os
import time
import logging import logging
import json import json
import sys import sys
import re import re
import urllib2
import Queue import Queue
import multiprocessing import multiprocessing
import gzip
from multiprocessing import Process from multiprocessing import Process
from multiprocessing import Pool from multiprocessing import Pool
@@ -30,6 +33,8 @@ from overviewer_core import logger
from overviewer_core import nbt from overviewer_core import nbt
from overviewer_core import configParser, world from overviewer_core import configParser, world
UUID_LOOKUP_URL = 'https://sessionserver.mojang.com/session/minecraft/profile/'
def replaceBads(s): def replaceBads(s):
"Replaces bad characters with good characters!" "Replaces bad characters with good characters!"
bads = [" ", "(", ")"] bads = [" ", "(", ")"]
@@ -114,25 +119,78 @@ def handleEntities(rset, outputdir, render, rname, config):
logging.info("Done.") logging.info("Done.")
def handlePlayers(rset, render, worldpath): class PlayerDict(dict):
use_uuid = False
_name = ''
uuid_cache = None # A cache the UUID->profile lookups
@classmethod
def load_cache(cls, outputdir):
cache_file = os.path.join(outputdir, "uuidcache.dat")
pid = multiprocessing.current_process().pid
if os.path.exists(cache_file):
gz = gzip.GzipFile(cache_file)
cls.uuid_cache = json.load(gz)
logging.info("Loaded UUID cache from %r with %d entries", cache_file, len(cls.uuid_cache.keys()))
else:
cls.uuid_cache = {}
logging.info("Initialized an empty UUID cache")
cls.save_cache(outputdir)
@classmethod
def save_cache(cls, outputdir):
cache_file = os.path.join(outputdir, "uuidcache.dat")
gz = gzip.GzipFile(cache_file, "wb")
json.dump(cls.uuid_cache, gz)
logging.info("Wrote UUID cache with %d entries", len(cls.uuid_cache.keys()))
def __getitem__(self, item):
if item == "EntityId":
if not super(PlayerDict, self).has_key("EntityId"):
if self.use_uuid:
super(PlayerDict, self).__setitem__("EntityId", self.get_name_from_uuid())
else:
super(PlayerDict, self).__setitem__("EntityId", self._name)
return super(PlayerDict, self).__getitem__(item)
def get_name_from_uuid(self):
sname = self._name.replace('-','')
try:
profile = PlayerDict.uuid_cache[sname]
return profile['name']
except (KeyError,):
pass
try:
profile = json.loads(urllib2.urlopen(UUID_LOOKUP_URL + sname).read())
if 'name' in profile:
PlayerDict.uuid_cache[sname] = profile
return profile['name']
except (ValueError, urllib2.URLError):
logging.warning("Unable to get player name for UUID %s", self._name)
def handlePlayers(rset, render, worldpath, outputdir):
if not hasattr(rset, "_pois"): if not hasattr(rset, "_pois"):
rset._pois = dict(TileEntities=[], Entities=[]) rset._pois = dict(TileEntities=[], Entities=[])
# only handle this region set once # only handle this region set once
if 'Players' in rset._pois: if 'Players' in rset._pois:
return return
dimension = None
try: if rset.get_type():
dimension = {None: 0, dimension = int(re.match(r"^DIM(_MYST)?(-?\d+)$", rset.get_type()).group(2))
'DIM-1': -1, else:
'DIM1': 1}[rset.get_type()] dimension = 0
except KeyError, e:
mystdim = re.match(r"^DIM_MYST(\d+)$", e.message) # Dirty hack. Woo! playerdir = os.path.join(worldpath, "playerdata")
if mystdim: useUUIDs = True
dimension = int(mystdim.group(1)) if not os.path.isdir(playerdir):
else: playerdir = os.path.join(worldpath, "players")
raise useUUIDs = False
playerdir = os.path.join(worldpath, "players")
if os.path.isdir(playerdir): if os.path.isdir(playerdir):
playerfiles = os.listdir(playerdir) playerfiles = os.listdir(playerdir)
playerfiles = [x for x in playerfiles if x.endswith(".dat")] playerfiles = [x for x in playerfiles if x.endswith(".dat")]
@@ -143,32 +201,40 @@ def handlePlayers(rset, render, worldpath):
isSinglePlayer = True isSinglePlayer = True
rset._pois['Players'] = [] rset._pois['Players'] = []
for playerfile in playerfiles: for playerfile in playerfiles:
try: try:
data = nbt.load(os.path.join(playerdir, playerfile))[1] data = PlayerDict(nbt.load(os.path.join(playerdir, playerfile))[1])
data.use_uuid = useUUIDs
if isSinglePlayer: if isSinglePlayer:
data = data['Data']['Player'] data = data['Data']['Player']
except IOError: except IOError:
logging.warning("Skipping bad player dat file %r", playerfile) logging.warning("Skipping bad player dat file %r", playerfile)
continue continue
playername = playerfile.split(".")[0] playername = playerfile.split(".")[0]
if isSinglePlayer: if isSinglePlayer:
playername = 'Player' playername = 'Player'
data._name = playername
if data['Dimension'] == dimension: if data['Dimension'] == dimension:
# Position at last logout # Position at last logout
data['id'] = "Player" data['id'] = "Player"
data['EntityId'] = playername
data['x'] = int(data['Pos'][0]) data['x'] = int(data['Pos'][0])
data['y'] = int(data['Pos'][1]) data['y'] = int(data['Pos'][1])
data['z'] = int(data['Pos'][2]) data['z'] = int(data['Pos'][2])
# Time at last logout, calculated from last time the player's file was modified
data['time'] = time.localtime(os.path.getmtime(os.path.join(playerdir, playerfile)))
rset._pois['Players'].append(data) rset._pois['Players'].append(data)
if "SpawnX" in data and dimension == 0: if "SpawnX" in data and dimension == 0:
# Spawn position (bed or main spawn) # Spawn position (bed or main spawn)
spawn = {"id": "PlayerSpawn", spawn = PlayerDict()
"EntityId": playername, spawn._name = playername
"x": data['SpawnX'], spawn["id"] = "PlayerSpawn"
"y": data['SpawnY'], spawn["x"] = data['SpawnX']
"z": data['SpawnZ']} spawn["y"] = data['SpawnY']
spawn["z"] = data['SpawnZ']
rset._pois['Players'].append(spawn) rset._pois['Players'].append(spawn)
def handleManual(rset, manualpois): def handleManual(rset, manualpois):
@@ -220,6 +286,8 @@ def main():
markersets = set() markersets = set()
markers = dict() markers = dict()
PlayerDict.load_cache(destdir)
for rname, render in config['renders'].iteritems(): for rname, render in config['renders'].iteritems():
try: try:
worldpath = config['worlds'][render['world']] worldpath = config['worlds'][render['world']]
@@ -259,7 +327,7 @@ def main():
if not options.skipscan: if not options.skipscan:
handleEntities(rset, os.path.join(destdir, rname), render, rname, config) handleEntities(rset, os.path.join(destdir, rname), render, rname, config)
handlePlayers(rset, render, worldpath) handlePlayers(rset, render, worldpath, destdir)
handleManual(rset, render['manualpois']) handleManual(rset, render['manualpois'])
logging.info("Done handling POIs") logging.info("Done handling POIs")
@@ -370,6 +438,8 @@ def main():
markerSetDict[name]['raw'].append(d) markerSetDict[name]['raw'].append(d)
#print markerSetDict #print markerSetDict
PlayerDict.save_cache(destdir)
with open(os.path.join(destdir, "markersDB.js"), "w") as output: with open(os.path.join(destdir, "markersDB.js"), "w") as output:
output.write("var markersDB=") output.write("var markersDB=")
json.dump(markerSetDict, output, indent=2) json.dump(markerSetDict, output, indent=2)

View File

@@ -119,7 +119,7 @@ overviewer.util = {
zoom = overviewer.mapView.options.currentTileSet.get('minZoom'); zoom = overviewer.mapView.options.currentTileSet.get('minZoom');
} else { } else {
zoom = parseInt(zoom); zoom = parseInt(zoom);
if (zoom < 0 && zoom + overviewer.mapView.options.currentTileSet.get('maxZoom') >= 0) { if (zoom < 0) {
// if zoom is negative, treat it as a "zoom out from max" // if zoom is negative, treat it as a "zoom out from max"
zoom += overviewer.mapView.options.currentTileSet.get('maxZoom'); zoom += overviewer.mapView.options.currentTileSet.get('maxZoom');
} else { } else {
@@ -127,6 +127,13 @@ overviewer.util = {
zoom = overviewer.mapView.options.currentTileSet.get('defaultZoom'); zoom = overviewer.mapView.options.currentTileSet.get('defaultZoom');
} }
} }
// clip zoom
if (zoom > overviewer.mapView.options.currentTileSet.get('maxZoom'))
zoom = overviewer.mapView.options.currentTileSet.get('maxZoom');
if (zoom < overviewer.mapView.options.currentTileSet.get('minZoom'))
zoom = overviewer.mapView.options.currentTileSet.get('minZoom');
overviewer.map.setZoom(zoom); overviewer.map.setZoom(zoom);
} }
@@ -512,9 +519,9 @@ overviewer.util = {
} }
if (zoom == currTileset.get('maxZoom')) { if (zoom >= currTileset.get('maxZoom')) {
zoom = 'max'; zoom = 'max';
} else if (zoom == currTileset.get('minZoom')) { } else if (zoom <= currTileset.get('minZoom')) {
zoom = 'min'; zoom = 'min';
} else { } else {
// default to (map-update friendly) negative zooms // default to (map-update friendly) negative zooms
@@ -556,7 +563,7 @@ overviewer.util = {
zoom = tsetModel.get('minZoom'); zoom = tsetModel.get('minZoom');
} else { } else {
zoom = parseInt(zoom); zoom = parseInt(zoom);
if (zoom < 0 && zoom + tsetModel.get('maxZoom') >= 0) { if (zoom < 0) {
// if zoom is negative, treat it as a "zoom out from max" // if zoom is negative, treat it as a "zoom out from max"
zoom += tsetModel.get('maxZoom'); zoom += tsetModel.get('maxZoom');
} else { } else {
@@ -565,6 +572,12 @@ overviewer.util = {
} }
} }
// clip zoom
if (zoom > tsetModel.get('maxZoom'))
zoom = tsetModel.get('maxZoom');
if (zoom < tsetModel.get('minZoom'))
zoom = tsetModel.get('minZoom');
overviewer.map.setCenter(latlngcoords); overviewer.map.setCenter(latlngcoords);
overviewer.map.setZoom(zoom); overviewer.map.setZoom(zoom);
var locationmarker = new overviewer.views.LocationIconView(); var locationmarker = new overviewer.views.LocationIconView();

View File

@@ -19,6 +19,7 @@ import tempfile
import shutil import shutil
import logging import logging
import stat import stat
import errno
default_caps = {"chmod_works": True, "rename_works": True} default_caps = {"chmod_works": True, "rename_works": True}
@@ -150,6 +151,20 @@ class FileReplacer(object):
else: else:
# copy permission bits, if needed # copy permission bits, if needed
if self.caps.get("chmod_works") and os.path.exists(self.destname): if self.caps.get("chmod_works") and os.path.exists(self.destname):
shutil.copymode(self.destname, self.tmpname) try:
shutil.copymode(self.destname, self.tmpname)
except OSError, e:
# Ignore errno ENOENT: file does not exist. Due to a race
# condition, two processes could conceivably try and update
# the same temp file at the same time
if e.errno != errno.ENOENT:
raise
# atomic rename into place # atomic rename into place
os.rename(self.tmpname, self.destname) try:
os.rename(self.tmpname, self.destname)
except OSError, e:
# Ignore errno ENOENT: file does not exist. Due to a race
# condition, two processes could conceivably try and update
# the same temp file at the same time
if e.errno != errno.ENOENT:
raise

View File

@@ -92,17 +92,50 @@ class LoggingObserver(Observer):
#this is an easy way to make the first update() call print a line #this is an easy way to make the first update() call print a line
self.last_update = -101 self.last_update = -101
# a fake ProgressBar, for the sake of ETA
class FakePBar(object):
def __init__(self):
self.maxval = None
self.currval = 0
self.finished = False
self.start_time = None
self.seconds_elapsed = 0
def finish(self):
self.update(self.maxval)
def update(self, value):
assert 0 <= value <= self.maxval
self.currval = value
if self.finished:
return False
if not self.start_time:
self.start_time = time.time()
self.seconds_elapsed = time.time() - self.start_time
if value == self.maxval:
self.finished = True
self.fake = FakePBar();
self.eta = progressbar.ETA()
def start(self, max_value):
self.fake.maxval = max_value
super(LoggingObserver, self).start(max_value)
def finish(self): def finish(self):
logging.info("Rendered %d of %d. %d%% complete", self.get_max_value(), self.fake.finish()
self.get_max_value(), 100.0) logging.info("Rendered %d of %d. %d%% complete. %s", self.get_max_value(),
self.get_max_value(), 100.0, self.eta.update(self.fake))
super(LoggingObserver, self).finish() super(LoggingObserver, self).finish()
def update(self, current_value): def update(self, current_value):
super(LoggingObserver, self).update(current_value) super(LoggingObserver, self).update(current_value)
self.fake.update(current_value)
if self._need_update(): if self._need_update():
logging.info("Rendered %d of %d. %d%% complete", logging.info("Rendered %d of %d. %d%% complete. %s",
self.get_current_value(), self.get_max_value(), self.get_current_value(), self.get_max_value(),
self.get_percentage()) self.get_percentage(), self.eta.update(self.fake))
self.last_update = current_value self.last_update = current_value
return True return True
return False return False
@@ -345,7 +378,7 @@ class ServerAnnounceObserver(Observer):
def update(self, current_value): def update(self, current_value):
super(ServerAnnounceObserver, self).update(current_value) super(ServerAnnounceObserver, self).update(current_value)
if self._need_update(current_value): if self._need_update():
self._send_output('Rendered %d of %d tiles, %d%% complete' % self._send_output('Rendered %d of %d tiles, %d%% complete' %
(self.get_current_value(), self.get_max_value(), (self.get_current_value(), self.get_max_value(),
self.get_percentage())) self.get_percentage()))

View File

@@ -16,37 +16,117 @@
import os import os
import subprocess import subprocess
import shlex import shlex
import logging
pngcrush = "pngcrush" class Optimizer:
optipng = "optipng" binaryname = ""
advdef = "advdef"
def check_programs(level): def __init__(self):
path = os.environ.get("PATH").split(os.pathsep) raise NotImplementedError("I can't let you do that, Dave.")
def exists_in_path(prog): def optimize(self, img):
result = filter(lambda x: os.path.exists(os.path.join(x, prog)), path) raise NotImplementedError("I can't let you do that, Dave.")
return len(result) != 0
for prog,l in [(pngcrush,1), (advdef,2)]: def fire_and_forget(self, args):
if l <= level: subprocess.check_call(args)
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): def check_availability(self):
if imgformat == 'png': path = os.environ.get("PATH").split(os.pathsep)
if optimizeimg >= 1:
# we can't do an atomic replace here because windows is terrible
# so instead, we make temp files, delete the old ones, and rename
# the temp files. go windows!
subprocess.Popen([pngcrush, imgpath, imgpath + ".tmp"],
stderr=subprocess.STDOUT, stdout=subprocess.PIPE).communicate()[0]
os.remove(imgpath)
os.rename(imgpath+".tmp", imgpath)
if optimizeimg >= 2: def exists_in_path(prog):
# the "-nc" it's needed to no broke the transparency of tiles result = filter(lambda x: os.path.exists(os.path.join(x, prog)), path)
recompress_option = "-z2" if optimizeimg == 2 else "-z4" return len(result) != 0
subprocess.Popen([advdef, recompress_option,imgpath], stderr=subprocess.STDOUT,
stdout=subprocess.PIPE).communicate()[0]
if (not exists_in_path(self.binaryname)) and (not exists_in_path(self.binaryname + ".exe")):
raise Exception("Optimization program '%s' was not found!" % self.binaryname)
def is_crusher(self):
"""Should return True if the optimization is lossless, i.e. none of the actual image data will be changed."""
raise NotImplementedError("I'm so abstract I can't even say whether I'm a crusher.")
class NonAtomicOptimizer(Optimizer):
def cleanup(self, img):
os.remove(img)
os.rename(img + ".tmp", img)
def fire_and_forget(self, args, img):
subprocess.check_call(args)
self.cleanup(img)
class PNGOptimizer:
def __init__(self):
raise NotImplementedError("I can't let you do that, Dave.")
class JPEGOptimizer:
def __init__(self):
raise NotImplementedError("I can't let you do that, Dave.")
class pngnq(NonAtomicOptimizer, PNGOptimizer):
binaryname = "pngnq"
def __init__(self, sampling=3, dither="n"):
if sampling < 1 or sampling > 10:
raise Exception("Invalid sampling value '%d' for pngnq!" % sampling)
if dither not in ["n", "f"]:
raise Exception("Invalid dither method '%s' for pngnq!" % dither)
self.sampling = sampling
self.dither = dither
def optimize(self, img):
if img.endswith(".tmp"):
extension = ".tmp"
else:
extension = ".png.tmp"
args = [self.binaryname, "-s", str(self.sampling), "-f", "-e", extension, img]
# Workaround for poopbuntu 12.04 which ships an old broken pngnq
if self.dither != "n":
args.insert(1, "-Q")
args.insert(2, self.dither)
NonAtomicOptimizer.fire_and_forget(self, args, img)
def is_crusher(self):
return False
class pngcrush(NonAtomicOptimizer, PNGOptimizer):
binaryname = "pngcrush"
# really can't be bothered to add some interface for all
# the pngcrush options, it sucks anyway
def __init__(self, brute=False):
self.brute = brute
def optimize(self, img):
args = [self.binaryname, img, img + ".tmp"]
if self.brute == True: # Was the user an idiot?
args.insert(1, "-brute")
NonAtomicOptimizer.fire_and_forget(self, args, img)
def is_crusher(self):
return True
class optipng(Optimizer, PNGOptimizer):
binaryname = "optipng"
def __init__(self, olevel=2):
self.olevel = olevel
def optimize(self, img):
Optimizer.fire_and_forget(self, [self.binaryname, "-o" + str(self.olevel), "-quiet", img])
def is_crusher(self):
return True
def optimize_image(imgpath, imgformat, optimizers):
for opt in optimizers:
if imgformat == 'png':
if isinstance(opt, PNGOptimizer):
opt.optimize(imgpath)
elif imgformat == 'jpg':
if isinstance(opt, JPEGOptimizer):
opt.optimize(imgpath)

View File

@@ -46,6 +46,7 @@
from settingsValidators import * from settingsValidators import *
import util import util
from observer import ProgressBarObserver, LoggingObserver, JSObserver from observer import ProgressBarObserver, LoggingObserver, JSObserver
from optimizeimages import pngnq, optipng, pngcrush
import platform import platform
import sys import sys
@@ -72,7 +73,7 @@ renders = Setting(required=True, default=util.OrderedDict(),
"imgquality": Setting(required=False, validator=validateImgQuality, default=95), "imgquality": Setting(required=False, validator=validateImgQuality, default=95),
"bgcolor": Setting(required=True, validator=validateBGColor, default="1a1a1a"), "bgcolor": Setting(required=True, validator=validateBGColor, default="1a1a1a"),
"defaultzoom": Setting(required=True, validator=validateDefaultZoom, default=1), "defaultzoom": Setting(required=True, validator=validateDefaultZoom, default=1),
"optimizeimg": Setting(required=True, validator=validateOptImg, default=0), "optimizeimg": Setting(required=True, validator=validateOptImg, default=[]),
"nomarkers": Setting(required=False, validator=validateBool, default=None), "nomarkers": Setting(required=False, validator=validateBool, default=None),
"texturepath": Setting(required=False, validator=validateTexturePath, default=None), "texturepath": Setting(required=False, validator=validateTexturePath, default=None),
"renderchecks": Setting(required=False, validator=validateInt, default=None), "renderchecks": Setting(required=False, validator=validateInt, default=None),

View File

@@ -5,7 +5,9 @@ from collections import namedtuple
import rendermodes import rendermodes
import util import util
from optimizeimages import Optimizer
from world import UPPER_LEFT, UPPER_RIGHT, LOWER_LEFT, LOWER_RIGHT from world import UPPER_LEFT, UPPER_RIGHT, LOWER_LEFT, LOWER_RIGHT
import logging
class ValidationException(Exception): class ValidationException(Exception):
pass pass
@@ -155,8 +157,30 @@ def validateBGColor(color):
return color return color
def validateOptImg(opt): def validateOptImg(optimizers):
return bool(opt) if isinstance(optimizers, (int, long)):
from optimizeimages import pngcrush
logging.warning("You're using a deprecated definition of optimizeimg. "\
"We'll do what you say for now, but please fix this as soon as possible.")
optimizers = [pngcrush()]
if not isinstance(optimizers, list):
raise ValidationException("What you passed to optimizeimg is not a list. "\
"Make sure you specify them like [foo()], with square brackets.")
if optimizers:
for opt, next_opt in zip(optimizers, optimizers[1:]) + [(optimizers[-1], None)]:
if not isinstance(opt, Optimizer):
raise ValidationException("Invalid Optimizer!")
opt.check_availability()
# Check whether the chaining is somewhat sane
if next_opt:
if opt.is_crusher() and not next_opt.is_crusher():
logging.warning("You're feeding a crushed output into an optimizer that does not crush. "\
"This is most likely pointless, and wastes time.")
return optimizers
def validateTexturePath(path): def validateTexturePath(path):
# Expand user dir in directories strings # Expand user dir in directories strings
@@ -201,15 +225,23 @@ def validateOutputDir(d):
return expand_path(d) return expand_path(d)
def validateCrop(value): def validateCrop(value):
if len(value) != 4: if not isinstance(value, list):
raise ValidationException("The value for the 'crop' setting must be a tuple of length 4") value = [value]
a, b, c, d = tuple(int(x) for x in value)
if a >= c: cropZones = []
a, c = c, a for zone in value:
if b >= d: if not isinstance(zone, tuple) or len(zone) != 4:
b, d = d, b raise ValidationException("The value for the 'crop' setting must be an array of tuples of length 4")
return (a, b, c, d) a, b, c, d = tuple(int(x) for x in zone)
if a >= c:
a, c = c, a
if b >= d:
b, d = d, b
cropZones.append((a, b, c, d))
return cropZones
def validateObserver(observer): def validateObserver(observer):
if all(map(lambda m: hasattr(observer, m), ['start', 'add', 'update', 'finish'])): if all(map(lambda m: hasattr(observer, m), ['start', 'add', 'update', 'finish'])):

View File

@@ -161,17 +161,6 @@ class Textures(object):
return None return None
if verbose: logging.info('search_zip_paths: ' + ', '.join(search_zip_paths)) if verbose: logging.info('search_zip_paths: ' + ', '.join(search_zip_paths))
# we've sucessfully loaded something from here before, so let's quickly try
# this before searching again
if self.jar is not None:
for jarfilename in search_zip_paths:
try:
self.jar.getinfo(jarfilename)
if verbose: logging.info("Found (cached) %s in '%s'", jarfilename, self.jarpath)
return self.jar.open(jarfilename)
except (KeyError, IOError), e:
pass
# A texture path was given on the command line. Search this location # A texture path was given on the command line. Search this location
# for the file first. # for the file first.
if self.find_file_local_path: if self.find_file_local_path:
@@ -227,6 +216,17 @@ class Textures(object):
if verbose: logging.info("Did not find the file in overviewer executable directory") if verbose: logging.info("Did not find the file in overviewer executable directory")
if verbose: logging.info("Looking for installed minecraft jar files...") if verbose: logging.info("Looking for installed minecraft jar files...")
# we've sucessfully loaded something from here before, so let's quickly try
# this before searching again
if self.jar is not None:
for jarfilename in search_zip_paths:
try:
self.jar.getinfo(jarfilename)
if verbose: logging.info("Found (cached) %s in '%s'", jarfilename, self.jarpath)
return self.jar.open(jarfilename)
except (KeyError, IOError), e:
pass
# Find an installed minecraft client jar and look in it for the texture # Find an installed minecraft client jar and look in it for the texture
# file we need. # file we need.
versiondir = "" versiondir = ""
@@ -638,23 +638,23 @@ class Textures(object):
increment = int(round((top[1] / 16.)*12.)) # range increment in the block height in pixels (half texture size) increment = int(round((top[1] / 16.)*12.)) # range increment in the block height in pixels (half texture size)
crop_height = increment crop_height = increment
top = top[0] top = top[0]
if side1 != None: if side1 is not None:
side1 = side1.copy() side1 = side1.copy()
ImageDraw.Draw(side1).rectangle((0, 0,16,crop_height),outline=(0,0,0,0),fill=(0,0,0,0)) ImageDraw.Draw(side1).rectangle((0, 0,16,crop_height),outline=(0,0,0,0),fill=(0,0,0,0))
if side2 != None: if side2 is not None:
side2 = side2.copy() side2 = side2.copy()
ImageDraw.Draw(side2).rectangle((0, 0,16,crop_height),outline=(0,0,0,0),fill=(0,0,0,0)) ImageDraw.Draw(side2).rectangle((0, 0,16,crop_height),outline=(0,0,0,0),fill=(0,0,0,0))
if side3 != None: if side3 is not None:
side3 = side3.copy() side3 = side3.copy()
ImageDraw.Draw(side3).rectangle((0, 0,16,crop_height),outline=(0,0,0,0),fill=(0,0,0,0)) ImageDraw.Draw(side3).rectangle((0, 0,16,crop_height),outline=(0,0,0,0),fill=(0,0,0,0))
if side4 != None: if side4 is not None:
side4 = side4.copy() side4 = side4.copy()
ImageDraw.Draw(side4).rectangle((0, 0,16,crop_height),outline=(0,0,0,0),fill=(0,0,0,0)) ImageDraw.Draw(side4).rectangle((0, 0,16,crop_height),outline=(0,0,0,0),fill=(0,0,0,0))
img = Image.new("RGBA", (24,24), self.bgcolor) img = Image.new("RGBA", (24,24), self.bgcolor)
# first back sides # first back sides
if side1 != None : if side1 is not None :
side1 = self.transform_image_side(side1) side1 = self.transform_image_side(side1)
side1 = side1.transpose(Image.FLIP_LEFT_RIGHT) side1 = side1.transpose(Image.FLIP_LEFT_RIGHT)
@@ -666,7 +666,7 @@ class Textures(object):
alpha_over(img, side1, (0,0), side1) alpha_over(img, side1, (0,0), side1)
if side2 != None : if side2 is not None :
side2 = self.transform_image_side(side2) side2 = self.transform_image_side(side2)
# Darken this side. # Darken this side.
@@ -676,12 +676,12 @@ class Textures(object):
alpha_over(img, side2, (12,0), side2) alpha_over(img, side2, (12,0), side2)
if bottom != None : if bottom is not None :
bottom = self.transform_image_top(bottom) bottom = self.transform_image_top(bottom)
alpha_over(img, bottom, (0,12), bottom) alpha_over(img, bottom, (0,12), bottom)
# front sides # front sides
if side3 != None : if side3 is not None :
side3 = self.transform_image_side(side3) side3 = self.transform_image_side(side3)
# Darken this side # Darken this side
@@ -691,7 +691,7 @@ class Textures(object):
alpha_over(img, side3, (0,6), side3) alpha_over(img, side3, (0,6), side3)
if side4 != None : if side4 is not None :
side4 = self.transform_image_side(side4) side4 = self.transform_image_side(side4)
side4 = side4.transpose(Image.FLIP_LEFT_RIGHT) side4 = side4.transpose(Image.FLIP_LEFT_RIGHT)
@@ -702,7 +702,7 @@ class Textures(object):
alpha_over(img, side4, (12,6), side4) alpha_over(img, side4, (12,6), side4)
if top != None : if top is not None :
top = self.transform_image_top(top) top = self.transform_image_top(top)
alpha_over(img, top, (0, increment), top) alpha_over(img, top, (0, increment), top)

View File

@@ -24,6 +24,7 @@ import functools
import time import time
import errno import errno
import stat import stat
import platform
from collections import namedtuple from collections import namedtuple
from itertools import product, izip, chain from itertools import product, izip, chain
@@ -129,6 +130,14 @@ Bounds = namedtuple("Bounds", ("mincol", "maxcol", "minrow", "maxrow"))
# slowest, but SHOULD be specified if this is the first render because # slowest, but SHOULD be specified if this is the first render because
# the scan will forgo tile stat calls. It's also useful for changing # the scan will forgo tile stat calls. It's also useful for changing
# texture packs or other options that effect the output. # texture packs or other options that effect the output.
# 3
# A very special mode. Using this will not actually render
# anything, but will leave this tileset in the resulting
# map. Useful for renders that you want to keep, but not
# update. Since this mode is so simple, it's left out of the
# rest of this discussion.
# #
# For 0 our caller has explicitly requested not to check mtimes on disk to # For 0 our caller has explicitly requested not to check mtimes on disk to
# speed things up. So the mode 0 chunk scan only looks at chunk mtimes and the # speed things up. So the mode 0 chunk scan only looks at chunk mtimes and the
@@ -237,6 +246,13 @@ class TileSet(object):
useful for changing texture packs or other options that effect useful for changing texture packs or other options that effect
the output. the output.
3
A very special mode. Using this will not actually render
anything, but will leave this tileset in the resulting
map. Useful for renders that you want to keep, but not
update. Since this mode is so simple, it's left out of the
rest of this discussion.
imgformat imgformat
A string indicating the output format. Must be one of 'png' or A string indicating the output format. Must be one of 'png' or
'jpeg' 'jpeg'
@@ -246,11 +262,7 @@ class TileSet(object):
relevant in jpeg mode. relevant in jpeg mode.
optimizeimg optimizeimg
an integer indiating optimizations to perform on png outputs. 0 A list of optimizer instances to use.
indicates no optimizations. Only relevant in png mode.
1 indicates pngcrush is run on all output images
2 indicates pngcrush and advdef are run on all output images with advdef -z2
3 indicates pngcrush and advdef are run on all output images with advdef -z4
rendermode rendermode
Perhaps the most important/relevant option: a string indicating the Perhaps the most important/relevant option: a string indicating the
@@ -389,6 +401,11 @@ class TileSet(object):
attribute for later use in iterate_work_items() attribute for later use in iterate_work_items()
""" """
# skip if we're told to
if self.options['renderchecks'] == 3:
return
# REMEMBER THAT ATTRIBUTES ASSIGNED IN THIS METHOD ARE NOT AVAILABLE IN # REMEMBER THAT ATTRIBUTES ASSIGNED IN THIS METHOD ARE NOT AVAILABLE IN
# THE do_work() METHOD (because this is only called in the main process # THE do_work() METHOD (because this is only called in the main process
# not the workers) # not the workers)
@@ -415,15 +432,16 @@ class TileSet(object):
return 1 return 1
def get_phase_length(self, phase): def get_phase_length(self, phase):
"""Returns the number of work items in a given phase, or None if there """Returns the number of work items in a given phase.
is no good estimate.
""" """
# Yeah functional programming! # Yeah functional programming!
# and by functional we mean a bastardized python switch statement
return { return {
0: lambda: self.dirtytree.count_all(), 0: lambda: self.dirtytree.count_all(),
#there is no good way to guess this so just give total count #there is no good way to guess this so just give total count
1: lambda: (4**(self.treedepth+1)-1)/3, 1: lambda: (4**(self.treedepth+1)-1)/3,
2: lambda: self.dirtytree.count_all(), 2: lambda: self.dirtytree.count_all(),
3: lambda: 0,
}[self.options['renderchecks']]() }[self.options['renderchecks']]()
def iterate_work_items(self, phase): def iterate_work_items(self, phase):
@@ -433,6 +451,10 @@ class TileSet(object):
This method returns an iterator over (obj, [dependencies, ...]) This method returns an iterator over (obj, [dependencies, ...])
""" """
# skip if asked to
if self.options['renderchecks'] == 3:
return
# The following block of code implementes the changelist functionality. # The following block of code implementes the changelist functionality.
fd = self.options.get("changelist", None) fd = self.options.get("changelist", None)
if fd: if fd:
@@ -536,6 +558,11 @@ class TileSet(object):
return "#%02x%02x%02x" % color[0:3] return "#%02x%02x%02x" % color[0:3]
isOverlay = self.options.get("overlay") or (not any(isinstance(x, rendermodes.Base) for x in self.options.get("rendermode"))) isOverlay = self.options.get("overlay") or (not any(isinstance(x, rendermodes.Base) for x in self.options.get("rendermode")))
# don't update last render time if we're leaving this alone
last_rendertime = self.last_rendertime
if self.options['renderchecks'] != 3:
last_rendertime = self.max_chunk_mtime
d = dict(name = self.options.get('title'), d = dict(name = self.options.get('title'),
zoomLevels = self.treedepth, zoomLevels = self.treedepth,
defaultZoom = self.options.get('defaultzoom'), defaultZoom = self.options.get('defaultzoom'),
@@ -545,13 +572,15 @@ class TileSet(object):
bgcolor = bgcolorformat(self.options.get('bgcolor')), bgcolor = bgcolorformat(self.options.get('bgcolor')),
world = self.options.get('worldname_orig') + world = self.options.get('worldname_orig') +
(" - " + self.options.get('dimension')[0] if self.options.get('dimension')[1] != 0 else ''), (" - " + self.options.get('dimension')[0] if self.options.get('dimension')[1] != 0 else ''),
last_rendertime = self.max_chunk_mtime, last_rendertime = last_rendertime,
imgextension = self.imgextension, imgextension = self.imgextension,
isOverlay = isOverlay, isOverlay = isOverlay,
poititle = self.options.get("poititle"), poititle = self.options.get("poititle"),
showlocationmarker = self.options.get("showlocationmarker") showlocationmarker = self.options.get("showlocationmarker")
) )
d['maxZoom'] = min(self.treedepth, d['maxZoom'])
d['minZoom'] = min(max(0, self.options.get("minzoom", 0)), d['maxZoom']) d['minZoom'] = min(max(0, self.options.get("minzoom", 0)), d['maxZoom'])
d['defaultZoom'] = max(d['minZoom'], min(d['defaultZoom'], d['maxZoom']))
if isOverlay: if isOverlay:
d.update({"tilesets": self.options.get("overlay")}) d.update({"tilesets": self.options.get("overlay")})
@@ -760,8 +789,8 @@ class TileSet(object):
# Compare the last modified time of the chunk and tile. If the # Compare the last modified time of the chunk and tile. If the
# tile is older, mark it in a RendertileSet object as dirty. # tile is older, mark it in a RendertileSet object as dirty.
for chunkx, chunkz, chunkmtime in self.regionset.iterate_chunks():
for chunkx, chunkz, chunkmtime in self.regionset.iterate_chunks() if (markall or platform.system() == 'Windows') else self.regionset.iterate_newer_chunks(last_rendertime):
chunkcount += 1 chunkcount += 1
if chunkmtime > max_chunk_mtime: if chunkmtime > max_chunk_mtime:
@@ -892,7 +921,11 @@ class TileSet(object):
try: try:
#quad = Image.open(path[1]).resize((192,192), Image.ANTIALIAS) #quad = Image.open(path[1]).resize((192,192), Image.ANTIALIAS)
src = Image.open(path[1]) src = Image.open(path[1])
# optimizeimg may have converted them to a palette image in the meantime
if src.mode != "RGB" and src.mode != "RGBA":
src = src.convert("RGBA")
src.load() src.load()
quad = Image.new("RGBA", (192, 192), self.options['bgcolor']) quad = Image.new("RGBA", (192, 192), self.options['bgcolor'])
resize_half(quad, src) resize_half(quad, src)
img.paste(quad, path[0]) img.paste(quad, path[0])
@@ -914,7 +947,14 @@ class TileSet(object):
if self.options['optimizeimg']: if self.options['optimizeimg']:
optimize_image(tmppath, imgformat, self.options['optimizeimg']) optimize_image(tmppath, imgformat, self.options['optimizeimg'])
os.utime(tmppath, (max_mtime, max_mtime)) try:
os.utime(tmppath, (max_mtime, max_mtime))
except OSError, e:
# Ignore errno ENOENT: file does not exist. Due to a race
# condition, two processes could conceivably try and update
# the same temp file at the same time
if e.errno != errno.ENOENT:
raise
def _render_rendertile(self, tile): def _render_rendertile(self, tile):
"""Renders the given render-tile. """Renders the given render-tile.

View File

@@ -48,7 +48,8 @@ def findGitHash():
import overviewer_version import overviewer_version
return overviewer_version.HASH return overviewer_version.HASH
except Exception: except Exception:
return "unknown" pass
return "unknown"
def findGitVersion(): def findGitVersion():
try: try:

View File

@@ -273,7 +273,7 @@ class RegionSet(object):
for x, y, regionfile in self._iterate_regionfiles(): for x, y, regionfile in self._iterate_regionfiles():
# regionfile is a pathname # regionfile is a pathname
self.regionfiles[(x,y)] = regionfile self.regionfiles[(x,y)] = (regionfile, os.path.getmtime(regionfile))
self.empty_chunk = [None,None] self.empty_chunk = [None,None]
logging.debug("Done scanning regions") logging.debug("Done scanning regions")
@@ -459,7 +459,7 @@ class RegionSet(object):
""" """
for (regionx, regiony), regionfile in self.regionfiles.iteritems(): for (regionx, regiony), (regionfile, filemtime) in self.regionfiles.iteritems():
try: try:
mcr = self._get_regionobj(regionfile) mcr = self._get_regionobj(regionfile)
except nbt.CorruptRegionError: except nbt.CorruptRegionError:
@@ -468,6 +468,27 @@ class RegionSet(object):
for chunkx, chunky in mcr.get_chunks(): for chunkx, chunky in mcr.get_chunks():
yield chunkx+32*regionx, chunky+32*regiony, mcr.get_chunk_timestamp(chunkx, chunky) yield chunkx+32*regionx, chunky+32*regiony, mcr.get_chunk_timestamp(chunkx, chunky)
def iterate_newer_chunks(self, mtime):
"""Returns an iterator over all chunk metadata in this world. Iterates
over tuples of integers (x,z,mtime) for each chunk. Other chunk data
is not returned here.
"""
for (regionx, regiony), (regionfile, filemtime) in self.regionfiles.iteritems():
""" SKIP LOADING A REGION WHICH HAS NOT BEEN MODIFIED! """
if (filemtime < mtime):
continue
try:
mcr = self._get_regionobj(regionfile)
except nbt.CorruptRegionError:
logging.warning("Found a corrupt region file at %s,%s. Skipping it.", regionx, regiony)
continue
for chunkx, chunky in mcr.get_chunks():
yield chunkx+32*regionx, chunky+32*regiony, mcr.get_chunk_timestamp(chunkx, chunky)
def get_chunk_mtime(self, x, z): def get_chunk_mtime(self, x, z):
"""Returns a chunk's mtime, or False if the chunk does not exist. This """Returns a chunk's mtime, or False if the chunk does not exist. This
is therefore a dual purpose method. It corrects for the given north is therefore a dual purpose method. It corrects for the given north
@@ -493,7 +514,7 @@ class RegionSet(object):
Coords can be either be global chunk coords, or local to a region Coords can be either be global chunk coords, or local to a region
""" """
regionfile = self.regionfiles.get((chunkX//32, chunkY//32),None) (regionfile,filemtime) = self.regionfiles.get((chunkX//32, chunkY//32),(None, None))
return regionfile return regionfile
def _iterate_regionfiles(self): def _iterate_regionfiles(self):
@@ -537,6 +558,8 @@ class RegionSetWrapper(object):
return self._r.get_chunk(x,z) return self._r.get_chunk(x,z)
def iterate_chunks(self): def iterate_chunks(self):
return self._r.iterate_chunks() return self._r.iterate_chunks()
def iterate_newer_chunks(self,filemtime):
return self._r.iterate_newer_chunks(filemtime)
def get_chunk_mtime(self, x, z): def get_chunk_mtime(self, x, z):
return self._r.get_chunk_mtime(x,z) return self._r.get_chunk_mtime(x,z)
@@ -623,6 +646,11 @@ class RotatedRegionSet(RegionSetWrapper):
x,z = self.rotate(x,z) x,z = self.rotate(x,z)
yield x,z,mtime yield x,z,mtime
def iterate_newer_chunks(self, filemtime):
for x,z,mtime in super(RotatedRegionSet, self).iterate_newer_chunks(filemtime):
x,z = self.rotate(x,z)
yield x,z,mtime
class CroppedRegionSet(RegionSetWrapper): class CroppedRegionSet(RegionSetWrapper):
def __init__(self, rsetobj, xmin, zmin, xmax, zmax): def __init__(self, rsetobj, xmin, zmin, xmax, zmax):
super(CroppedRegionSet, self).__init__(rsetobj) super(CroppedRegionSet, self).__init__(rsetobj)
@@ -646,6 +674,14 @@ class CroppedRegionSet(RegionSetWrapper):
self.xmin <= x <= self.xmax and self.xmin <= x <= self.xmax and
self.zmin <= z <= self.zmax self.zmin <= z <= self.zmax
) )
def iterate_newer_chunks(self, filemtime):
return ((x,z,mtime) for (x,z,mtime) in super(CroppedRegionSet,self).iterate_newer_chunks(filemtime)
if
self.xmin <= x <= self.xmax and
self.zmin <= z <= self.zmax
)
def get_chunk_mtime(self,x,z): def get_chunk_mtime(self,x,z):
if ( if (
self.xmin <= x <= self.xmax and self.xmin <= x <= self.xmax and
@@ -744,12 +780,7 @@ def get_worlds():
if not os.path.exists(world_dat): continue if not os.path.exists(world_dat): continue
info = nbt.load(world_dat)[1] info = nbt.load(world_dat)[1]
info['Data']['path'] = os.path.join(save_dir, dir).decode(loc) info['Data']['path'] = os.path.join(save_dir, dir).decode(loc)
if dir.startswith("World") and len(dir) == 6:
try:
world_n = int(dir[-1])
ret[world_n] = info['Data']
except ValueError:
pass
if 'LevelName' in info['Data'].keys(): if 'LevelName' in info['Data'].keys():
ret[info['Data']['LevelName']] = info['Data'] ret[info['Data']['LevelName']] = info['Data']

View File

@@ -1,6 +1,7 @@
#!/usr/bin/env python #!/usr/bin/env python
import sys import sys
import traceback
# quick version check # quick version check
if not (sys.version_info[0] == 2 and sys.version_info[1] >= 6): if not (sys.version_info[0] == 2 and sys.version_info[1] >= 6):
@@ -272,6 +273,7 @@ class CustomBuild(build):
build.run(self) build.run(self)
print("\nBuild Complete") print("\nBuild Complete")
except Exception: except Exception:
traceback.print_exc(limit=1)
print("\nFailed to build Overviewer!") print("\nFailed to build Overviewer!")
print("Please review the errors printed above and the build instructions") print("Please review the errors printed above and the build instructions")
print("at <http://docs.overviewer.org/en/latest/building/>. If you are") print("at <http://docs.overviewer.org/en/latest/building/>. If you are")

View File

@@ -53,6 +53,10 @@ class FakeRegionset(object):
for (x,z),mtime in self.chunks.iteritems(): for (x,z),mtime in self.chunks.iteritems():
yield x,z,mtime yield x,z,mtime
def iterate_newer_chunks(self, filemtime):
for (x,z),mtime in self.chunks.iteritems():
yield x,z,mtime
def get_chunk_mtime(self, x, z): def get_chunk_mtime(self, x, z):
try: try:
return self.chunks[x,z] return self.chunks[x,z]