Merge branch 'master' into snapshot
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
577
docs/config.rst
577
docs/config.rst
@@ -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::
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
16
docs/faq.rst
16
docs/faq.rst
@@ -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
|
||||||
---------------------------------------------------------
|
---------------------------------------------------------
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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,7 +143,27 @@ 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
|
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
|
||||||
|
|
||||||
|
|
||||||
if options.pid:
|
if options.pid:
|
||||||
if os.path.exists(options.pid):
|
if os.path.exists(options.pid):
|
||||||
@@ -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,6 +512,7 @@ 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
|
||||||
|
for rset in rsets:
|
||||||
tset = tileset.TileSet(w, rset, assetMrg, tex, tileSetOpts, tileset_dir)
|
tset = tileset.TileSet(w, rset, assetMrg, tex, tileSetOpts, tileset_dir)
|
||||||
tilesets.append(tset)
|
tilesets.append(tset)
|
||||||
|
|
||||||
@@ -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():
|
||||||
|
|||||||
@@ -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,
|
|
||||||
'DIM1': 1}[rset.get_type()]
|
|
||||||
except KeyError, e:
|
|
||||||
mystdim = re.match(r"^DIM_MYST(\d+)$", e.message) # Dirty hack. Woo!
|
|
||||||
if mystdim:
|
|
||||||
dimension = int(mystdim.group(1))
|
|
||||||
else:
|
else:
|
||||||
raise
|
dimension = 0
|
||||||
|
|
||||||
|
playerdir = os.path.join(worldpath, "playerdata")
|
||||||
|
useUUIDs = True
|
||||||
|
if not os.path.isdir(playerdir):
|
||||||
playerdir = os.path.join(worldpath, "players")
|
playerdir = os.path.join(worldpath, "players")
|
||||||
|
useUUIDs = False
|
||||||
|
|
||||||
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)
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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):
|
||||||
|
try:
|
||||||
shutil.copymode(self.destname, self.tmpname)
|
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
|
||||||
|
try:
|
||||||
os.rename(self.tmpname, self.destname)
|
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
|
||||||
|
|||||||
@@ -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):
|
def finish(self):
|
||||||
logging.info("Rendered %d of %d. %d%% complete", self.get_max_value(),
|
self.update(self.maxval)
|
||||||
self.get_max_value(), 100.0)
|
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):
|
||||||
|
self.fake.finish()
|
||||||
|
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()))
|
||||||
|
|||||||
@@ -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):
|
||||||
|
raise NotImplementedError("I can't let you do that, Dave.")
|
||||||
|
|
||||||
|
def optimize(self, img):
|
||||||
|
raise NotImplementedError("I can't let you do that, Dave.")
|
||||||
|
|
||||||
|
def fire_and_forget(self, args):
|
||||||
|
subprocess.check_call(args)
|
||||||
|
|
||||||
|
def check_availability(self):
|
||||||
path = os.environ.get("PATH").split(os.pathsep)
|
path = os.environ.get("PATH").split(os.pathsep)
|
||||||
|
|
||||||
def exists_in_path(prog):
|
def exists_in_path(prog):
|
||||||
result = filter(lambda x: os.path.exists(os.path.join(x, prog)), path)
|
result = filter(lambda x: os.path.exists(os.path.join(x, prog)), path)
|
||||||
return len(result) != 0
|
return len(result) != 0
|
||||||
|
|
||||||
for prog,l in [(pngcrush,1), (advdef,2)]:
|
if (not exists_in_path(self.binaryname)) and (not exists_in_path(self.binaryname + ".exe")):
|
||||||
if l <= level:
|
raise Exception("Optimization program '%s' was not found!" % self.binaryname)
|
||||||
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 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 imgformat == 'png':
|
||||||
if optimizeimg >= 1:
|
if isinstance(opt, PNGOptimizer):
|
||||||
# we can't do an atomic replace here because windows is terrible
|
opt.optimize(imgpath)
|
||||||
# so instead, we make temp files, delete the old ones, and rename
|
elif imgformat == 'jpg':
|
||||||
# the temp files. go windows!
|
if isinstance(opt, JPEGOptimizer):
|
||||||
subprocess.Popen([pngcrush, imgpath, imgpath + ".tmp"],
|
opt.optimize(imgpath)
|
||||||
stderr=subprocess.STDOUT, stdout=subprocess.PIPE).communicate()[0]
|
|
||||||
os.remove(imgpath)
|
|
||||||
os.rename(imgpath+".tmp", imgpath)
|
|
||||||
|
|
||||||
if optimizeimg >= 2:
|
|
||||||
# the "-nc" it's needed to no broke the transparency of tiles
|
|
||||||
recompress_option = "-z2" if optimizeimg == 2 else "-z4"
|
|
||||||
subprocess.Popen([advdef, recompress_option,imgpath], stderr=subprocess.STDOUT,
|
|
||||||
stdout=subprocess.PIPE).communicate()[0]
|
|
||||||
|
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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)
|
|
||||||
|
cropZones = []
|
||||||
|
for zone in value:
|
||||||
|
if not isinstance(zone, tuple) or len(zone) != 4:
|
||||||
|
raise ValidationException("The value for the 'crop' setting must be an array of tuples of length 4")
|
||||||
|
a, b, c, d = tuple(int(x) for x in zone)
|
||||||
|
|
||||||
if a >= c:
|
if a >= c:
|
||||||
a, c = c, a
|
a, c = c, a
|
||||||
if b >= d:
|
if b >= d:
|
||||||
b, d = d, b
|
b, d = d, b
|
||||||
return (a, b, c, d)
|
|
||||||
|
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'])):
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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'])
|
||||||
|
|
||||||
|
try:
|
||||||
os.utime(tmppath, (max_mtime, max_mtime))
|
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.
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ def findGitHash():
|
|||||||
import overviewer_version
|
import overviewer_version
|
||||||
return overviewer_version.HASH
|
return overviewer_version.HASH
|
||||||
except Exception:
|
except Exception:
|
||||||
|
pass
|
||||||
return "unknown"
|
return "unknown"
|
||||||
|
|
||||||
def findGitVersion():
|
def findGitVersion():
|
||||||
|
|||||||
@@ -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']
|
||||||
|
|
||||||
|
|||||||
2
setup.py
2
setup.py
@@ -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")
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
Reference in New Issue
Block a user