diff --git a/docs/config.rst b/docs/config.rst index c617f52..44bb5fe 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -376,6 +376,31 @@ Observers once for every 1% of progress. **Required** + + ``RConObserver(target, password[, port][, pct_interval])`` + This Observer will announce render progress with the server's ``say`` + command through RCon. + + * ``target=
`` + Address of the target Minecraft server. + + **Required** + + * ``password=`` + The server's rcon password. + + **Required** + + * ``port=`` + Port on which the Minecraft server listens for incoming RCon connections. + + **Default:** ``25575`` + + * ``pct_interval=`` + Percentage interval in which the progress should be announced, the same as + for ``ServerAnnounceObserver``. + + **Default:** ``10`` @@ -683,8 +708,8 @@ Image options 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 + optimize the filesize of rendered 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 @@ -766,6 +791,34 @@ Image options **Default:** ``False`` + ``jpegoptim`` + jpegoptim can do both lossy and lossless JPEG optimisation. If no options are specified, + jpegoptim will only do lossless optimisations. + Available settings: + + ``quality`` + A number between 0 and 100 that corresponds to the jpeg quality level. If the input + image has a lower quality specified than the output image, jpegoptim will only do + lossless optimisations. + + If this option is specified and the above condition does not apply, jpegoptim will + do lossy optimisation. + + **Default:** ``None`` *(= Unspecified)* + + ``target_size`` + Either a percentage of the original filesize (e.g. ``"50%"``) or a target filesize + in kilobytes (e.g. ``15``). jpegoptim will then try to reach this as its target size. + + If specified, jpegoptim will do lossy optimisation. + + .. warning:: + This appears to have a greater performance impact than just setting ``quality``. + Unless predictable filesizes are a thing you need, you should probably use ``quality`` + instead. + + **Default:** ``None`` *(= Unspecified)* + **Default:** ``[]`` Zoom @@ -1099,6 +1152,32 @@ MineralOverlay MineralOverlay(minerals=[(64,(255,255,0)), (13,(127,0,127))]) +StructureOverlay + Color the map according to patterns of blocks. With this rail overlays + or overlays for other small structures can be realized. It can also be + a MineralOverlay with alpha support. + + This Overlay colors according to a patterns that are specified as + multiple tuples of the form ``(relx, rely, relz, blockid)``. So + by specifying ``(0, -1, 0, 4)`` the block below the current one has to + be a cobblestone. + + One color is then specified as + ``((relblockid1, relblockid2, ...), (r, g, b, a))`` where the + ``relblockid*`` are relative coordinates and the blockid as specified + above. The ``relblockid*`` must match all at the same time for the + color to apply. + + Example:: + + MineralOverlay(minerals=[(((0, 0, 0, 66), (0, -1, 0, 4)), (255, 0, 0, 255)), + (((0, 0, 0, 27), (0, -1, 0, 4)), (0, 255, 0, 255))]) + + In this example all rails(66) on top of cobblestone are rendered in + pure red. And all powerrails(27) are rendered in green. + + If ``minerals`` is not provided, a default rail coloring is used. + BiomeOverlay Color the map according to the biome at that point. Either use on top of other modes or on top of ClearBase to create a pure overlay. diff --git a/docs/running.rst b/docs/running.rst index 340f1e5..0e7acd3 100644 --- a/docs/running.rst +++ b/docs/running.rst @@ -78,7 +78,7 @@ Usage ===== For this section, we assume the executable is ``overviewer.py``. Replace that -with ``overviewer.exe`` for windows. +with ``overviewer.exe`` for windows. Overviewer usage:: @@ -131,7 +131,7 @@ is typically correct. is not detected. This means that changes in your render configuration will not be reflected in your world except in updated chunks. It could also cause problems if the system clock of the machine running Minecraft is not stable. - + **This option is the default** unless :option:`--forcerender` or :option:`--check-tiles` is in effect. This option conflicts with :option:`--forcerender` and :option:`--check-tiles`. @@ -148,13 +148,13 @@ is typically correct. automatically stored. This option was designed to handle the case where the last render was interrupted -- some tiles have been updated but others haven't, so each one is checked before it is rendered. - + This is slightly slower than :option:`--no-tile-checks` due to the additional disk-io involved in reading tile mtimes from the filesystem - + Since this option also checks for erroneous tiles, **It is also useful after you delete sections of your map, e.g. with worldedit, to delete tiles that - should no longer exist.** Overviewer greatly overestimates tiles to be + should no longer exist.** Overviewer greatly overestimates tiles to be rendered and time needed to complete. The caveats with this option are the same as for :option:`--no-tile-checks` @@ -186,15 +186,14 @@ is typically correct. conflicts with :option:`--check-tiles` and :option:`--no-tile-checks` .. cmdoption:: --genpoi - + .. note:: Don't use this flag without first reading :ref:`signsmarkers`! - Generates the POI markers for your map. This option does not do any tile/map - generation, and ONLY generates markers. See :ref:`signsmarkers` on how to + Generates the POI markers for your map. This option does not do any tile/map + generation, and ONLY generates markers. See :ref:`signsmarkers` on how to configure POI options. - .. cmdoption:: -p , --processes This specifies the number of worker processes to spawn on the local machine @@ -203,6 +202,17 @@ is typically correct. This option can also be specified in the config file as :ref:`processes ` +.. cmdoption:: --skip-scan + + .. note:: + Don't use this flag without first reading :ref:`signsmarkers`! + + When generating POI markers, this option prevents scanning for entities and + tile entities, and only creates the markers specified in the config file. + This considerably speeds up the POI marker generation process if no entities + or tile entities are being used for POI markers. See :ref:`signsmarkers` on + how to configure POI options. + .. cmdoption:: -v, --verbose Activate a more verbose logging format and turn on debugging output. This @@ -237,7 +247,7 @@ Installing the Textures If Overviewer is running on a machine with the Minecraft client installed, it will automatically use the default textures from Minecraft. - + .. note:: Overviewer will only search for installed client *release* versions, not snapshots. If you want to use a snapshot client jar for the textures, @@ -263,21 +273,21 @@ If you want or need to provide your own textures, you have several options: VERSION=1.8 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 a terminal to get 1.8 textures:: - + wget https://s3.amazonaws.com/Minecraft.Download/versions/1.8/1.8.jar -P ~/.minecraft/versions/1.8/ * You can also just run the launcher to install the client. - + * You can transfer the client jar to the correct place manually, from a computer that does have the client, to your server. The correct places are: - + * For Linux: ``~/.minecraft/versions//.jar`` * For Mac: ``~/Library/Application Support/minecraft/versions//.jar`` - + * For Windows: ``%APPDATA%/.minecraft/versions//.jar`` * You can download and use a custom resource pack. Download the resource pack diff --git a/overviewer.py b/overviewer.py index 50c37e8..275c0dc 100755 --- a/overviewer.py +++ b/overviewer.py @@ -50,6 +50,16 @@ def main(): # bootstrap the logger with defaults logger.configure() + if os.name == "posix": + if os.geteuid() == 0: + logging.warning("You are running Overviewer as root. " + "It is recommended that you never do this, " + "as it is dangerous for your system. If you are running " + "into permission errors, fix your file/directory " + "permissions instead. Overviewer does not need access to " + "critical system resources and therefore does not require " + "root access.") + try: cpus = multiprocessing.cpu_count() except NotImplementedError: @@ -478,8 +488,8 @@ dir but you forgot to put quotes around the directory, since it contains spaces. logging.error("Sorry, I can't find anything to render! Are you sure there are .mca files in the world directory?") return 1 if rset == None: # indicates no such dimension was found: - logging.error("Sorry, you requested dimension '%s' for %s, but I couldn't find it", render['dimension'][0], render_name) - return 1 + logging.warn("Sorry, you requested dimension '%s' for %s, but I couldn't find it", render['dimension'][0], render_name) + continue ################# # Apply any regionset transformations here @@ -518,6 +528,12 @@ dir but you forgot to put quotes around the directory, since it contains spaces. tset = tileset.TileSet(w, rset, assetMrg, tex, tileSetOpts, tileset_dir) tilesets.append(tset) + # If none of the requested dimenstions exist, tilesets will be empty + if not tilesets: + logging.error("There are no tilesets to render! There's nothing to do, so exiting.") + return 1 + + # Do tileset preprocessing here, before we start dispatching jobs logging.info("Preprocessing...") for ts in tilesets: diff --git a/overviewer_core/aux_files/genPOI.py b/overviewer_core/aux_files/genPOI.py index 3e275ea..c46e143 100755 --- a/overviewer_core/aux_files/genPOI.py +++ b/overviewer_core/aux_files/genPOI.py @@ -385,8 +385,8 @@ def main(): # get the regionset for this dimension rset = w.get_regionset(render['dimension'][1]) if rset == None: # indicates no such dimension was found: - logging.error("Sorry, you requested dimension '%s' for the render '%s', but I couldn't find it", render['dimension'][0], rname) - return 1 + logging.warn("Sorry, you requested dimension '%s' for the render '%s', but I couldn't find it", render['dimension'][0], rname) + continue # find filters for this render for f in render['markers']: @@ -462,6 +462,7 @@ def main(): with open(os.path.join(destdir, "baseMarkers.js"), "w") as output: output.write("overviewer.util.injectMarkerScript('markersDB.js');\n") output.write("overviewer.util.injectMarkerScript('markers.js');\n") + output.write("overviewer.util.injectMarkerScript('regions.js');\n") output.write("overviewer.collections.haveSigns=true;\n") logging.info("Done") diff --git a/overviewer_core/data/js_src/views.js b/overviewer_core/data/js_src/views.js index d5f6ea6..6259223 100644 --- a/overviewer_core/data/js_src/views.js +++ b/overviewer_core/data/js_src/views.js @@ -496,6 +496,26 @@ overviewer.views.SignControlView = Backbone.View.extend({ 'strokeColor': entity['strokeColor'] }); dataRoot[i].markerObjs.push(polyline); + } + + // Polygons + if (typeof entity['polygon'] != 'undefined') { + var polypath = new Array(); + for (point in entity.polygon) { + polypath.push(overviewer.util.fromWorldToLatLng(entity.polygon[point].x, entity.polygon[point].y, entity.polygon[point].z, overviewer.mapView.options.currentTileSet)); + } + + var polygon = new google.maps.Polygon({ + 'clickable': false, + 'fillColor': entity['fillColor'], + 'fillOpacity': entity['fillOpacity'], + 'map': overviewer.map, + 'path': polypath, + 'strokeColor': entity['strokeColor'], + 'strokeOpacity': entity['strokeOpacity'], + 'visible': false + }); + dataRoot[i].markerObjs.push(polygon); } } dataRoot[i].created = true; diff --git a/overviewer_core/data/web_assets/regions.js b/overviewer_core/data/web_assets/regions.js index acd83e2..cef6ff0 100644 --- a/overviewer_core/data/web_assets/regions.js +++ b/overviewer_core/data/web_assets/regions.js @@ -1,9 +1,34 @@ // This is just an example. You can run some scripts to generate regions that -// will be drawn here. -overviewer.collections.regionDatas.push([ - // {"color": "#FFAA00", "opacity": 0.5, "closed": true, "path": [ - // {"x": 0, "y": 0, "z": 0}, - // {"x": 0, "y": 10, "z": 0}, - // {"x": 0, "y": 0, "z": 10} - // ]}, -]); +/* +var groupName = "Regions"; +var displayName = "Areas"; +var world = "top"; + +markersDB[groupName] = { + "raw": [ + { + "fillColor": "#00FF00", + "fillOpacity": 0.2, + "strokeColor": "#FF0000", + "strokeOpacity": 1, + "polygon" : [ + {"x": 347, "y": 67, "z": 95}, + {"x": 347, "y": 77, "z": 95}, + {"x": 347, "y": 77, "z": 105}, + {"x": 347, "y": 67, "z": 105}, + {"x": 347, "y": 67, "z": 105} + ]} + ], + "name": "Regions", + "created": false +} + +markers[world].push( + { + "groupName": groupName, + "icon": "signpost_icon.png", + "createInfoWindow": false, + "displayName": displayName, + "checked": true + }); +*/ \ No newline at end of file diff --git a/overviewer_core/nbt.py b/overviewer_core/nbt.py index cadeccf..d892226 100644 --- a/overviewer_core/nbt.py +++ b/overviewer_core/nbt.py @@ -65,7 +65,9 @@ class NBTFileReader(object): # compile the unpacker's into a classes _byte = struct.Struct("b") _short = struct.Struct(">h") + _ushort = struct.Struct(">H") _int = struct.Struct(">i") + _uint = struct.Struct(">I") _long = struct.Struct(">q") _float = struct.Struct(">f") _double = struct.Struct(">d") @@ -128,17 +130,17 @@ class NBTFileReader(object): return self._double.unpack(bytes)[0] def _read_tag_byte_array(self): - length = self._read_tag_int() + length = self._uint.unpack(self._file.read(4))[0] bytes = self._file.read(length) return bytes def _read_tag_int_array(self): - length = self._read_tag_int() + length = self._uint.unpack(self._file.read(4))[0] int_bytes = self._file.read(length*4) return struct.unpack(">%ii" % length, int_bytes) def _read_tag_string(self): - length = self._read_tag_short() + length = self._ushort.unpack(self._file.read(2))[0] # Read the string string = self._file.read(length) # decode it and return @@ -146,7 +148,7 @@ class NBTFileReader(object): def _read_tag_list(self): tagid = self._read_tag_byte() - length = self._read_tag_int() + length = self._uint.unpack(self._file.read(4))[0] read_method = self._read_tagmap[tagid] l = [] diff --git a/overviewer_core/observer.py b/overviewer_core/observer.py index e17c8f5..e068acb 100644 --- a/overviewer_core/observer.py +++ b/overviewer_core/observer.py @@ -19,6 +19,7 @@ import progressbar import sys import os import json +import rcon class Observer(object): """Base class that defines the observer interface. @@ -392,3 +393,43 @@ class ServerAnnounceObserver(Observer): self.target_handle.write('say %s\n' % output) self.target_handle.flush() + +# Fair amount of code duplication incoming +# Perhaps both ServerAnnounceObserver and RConObserver +# could share the percentage interval code. + +class RConObserver(Observer): + """Send the output to a Minecraft server via rcon""" + + def __init__(self, target, password, port=25575, pct_interval=10): + self.pct_interval = pct_interval + self.conn = rcon.RConConnection(target, port) + self.conn.login(password) + self.last_update = 0 + super(RConObserver, self).__init__() + + def start(self, max_value): + self._send_output("Starting render of %d total tiles" % max_value) + super(RConObserver, self).start(max_value) + + def finish(self): + self._send_output("Render complete!") + super(RConObserver, self).finish() + self.conn.close() + + def update(self, current_value): + super(RConObserver, self).update(current_value) + if self._need_update(): + self._send_output('Rendered %d of %d tiles, %d%% complete' % + (self.get_current_value(), self.get_max_value(), + self.get_percentage())) + self.last_update = current_value + + def _need_update(self): + return self.get_percentage() - \ + (self.last_update * 100.0 / self.get_max_value()) >= self.pct_interval + + def _send_output(self, output): + self.conn.command("say", output) + + diff --git a/overviewer_core/optimizeimages.py b/overviewer_core/optimizeimages.py index 0ccd4a2..af56039 100644 --- a/overviewer_core/optimizeimages.py +++ b/overviewer_core/optimizeimages.py @@ -120,6 +120,42 @@ class optipng(Optimizer, PNGOptimizer): def is_crusher(self): return True + +class jpegoptim(Optimizer, JPEGOptimizer): + binaryname = "jpegoptim" + crusher = True + quality = None + target_size = None + + def __init__(self, quality = None, target_size = None): + if quality is not None: + if quality < 0 or quality > 100: + raise Exception("Invalid target quality %d for jpegoptim" % quality) + self.quality = quality + + if target_size is not None: + self.target_size = target_size + + def optimize(self, img): + args = [self.binaryname, "-q", "-p"] + if self.quality is not None: + args.append("-m" + str(self.quality)) + + if self.target_size is not None: + args.append("-S" + str(self.target_size)) + + args.append(img) + + Optimizer.fire_and_forget(self, args) + + def is_crusher(self): + # Technically, optimisation is lossless if input image quality + # is below target quality, but this is irrelevant in this case + if (self.quality is not None) or (self.target_size is not None): + return False + else: + return True + def optimize_image(imgpath, imgformat, optimizers): diff --git a/overviewer_core/rcon.py b/overviewer_core/rcon.py new file mode 100644 index 0000000..52a1434 --- /dev/null +++ b/overviewer_core/rcon.py @@ -0,0 +1,95 @@ +# This file is part of the Minecraft Overviewer. +# +# Minecraft Overviewer is free software: you can redistribute it and/or +# modify it under the terms of the GNU General Public License as published +# by the Free Software Foundation, either version 3 of the License, or (at +# your option) any later version. +# +# Minecraft Overviewer is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +# Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with the Overviewer. If not, see . + +import socket +#from enum import Enum +import struct +import select + +class RConException(Exception): + def __init__(self, request_id, reason): + self.request_id = request_id + self.reason = reason + + def __str__(self): + return ("Failed RCon request with request ID %d, reason %s" % + (self.request_id, self.reason)) + +# In D, enums are just that, enums. They're a group of named constants, +# sometimes with a tag, sometimes anonymous. +# In Python, Enums use the same syntax as class objects that derive from +# the "Enum" base class, even though they are not normal python classes +# and work as singletons anyway, but instead of using a different syntax, +# Python instead decided to have a chapter in their docs about how Enums +# are different from regular classes while looking exactly the same. +# You can look at said document of failure right here: +# https://docs.python.org/3/library/enum.html#how-are-enums-different +# +# "D has too much shit going on for me" -- agrif, 2014 +# +# Fortunately, we're not allowed to use Enums in Python 2. + +#class RConType(Enum): +# command = 2 +# login = 3 + + +class RConConnection(): + rid = 0 + + def __init__(self, target, port): + self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.sock.connect((target, port)) + + def send(self, t, payload): + self.rid = self.rid + 1 + header = struct.pack(". + */ + +#include "overlay.h" + +typedef enum { false, true } bool; + +typedef struct { + /* inherits from overlay */ + RenderPrimitiveOverlay parent; + void *structures; + int numcolors; +} RenderPrimitiveStructure; + +struct Condition{ + int relx, rely, relz; + unsigned char block; +}; + +struct Color { + int numconds; + struct Condition *conditions; + unsigned char r, g, b, a; +}; + +static void get_color(void *data, + RenderState *state, + unsigned char *r, + unsigned char *g, + unsigned char *b, + unsigned char *a) { + /** + * Calculate the color at the current position and store the values to r,g,b,a. + **/ + RenderPrimitiveStructure *self = (RenderPrimitiveStructure *)data; + int x = state->x, z = state->z, y_max, y, col, cond; + struct Color *structures = (struct Color *)(self->structures); + struct Condition * c = NULL; + bool all = true; + y_max = state->y + 1; + + /** + * Check for every color in every y level if all its Conditions are met. + * If all conditions are met for one y level set r,b,g,a accordingly. + **/ + // iterate over all the colors + for ( col = 0; col < self->numcolors; col++) { + // iterate over all y levels + for (y = state->chunky * -16; y <= y_max; y++) { + // iterate over all the conditions + for (cond = 0; cond < structures[col].numconds; cond++) { + all = true; + c = (struct Condition *)&structures[col].conditions[cond]; + // check if the condition does apply and break from the conditions loop if not. + if(!(c->block == get_data(state, BLOCKS, x+c->relx, y+c->rely, z+c->relz))) { + all = false; + break; + } + } + if (all){ + // set the color + *r = structures[col].r; + *g = structures[col].g; + *b = structures[col].b; + *a = structures[col].a; + return; + } + } + } + return; +} + +static int overlay_structure_start(void *data, RenderState *state, PyObject *support) { + /** + * Initializing the search for structures by parsing the arguments and storing them into + * appropriate structures. If no arguments are passed create and use default values. + **/ + PyObject *opt; + RenderPrimitiveStructure* self; + + /* first, chain up */ + int ret = primitive_overlay.start(data, state, support); + if (ret != 0) + return ret; + + /* now do custom initializations */ + self = (RenderPrimitiveStructure *)data; + + // opt is a borrowed reference. do not deref + // store the structures python object into opt. + if (!render_mode_parse_option(support, "structures", "O", &(opt))) + return 1; + + /** + * Check if a sane option was passed. + **/ + if (opt && opt != Py_None) { + struct Color *structures = NULL; + struct Condition *cond = NULL; + Py_ssize_t structures_size = 0, i, cond_size = 0, n = 0; + bool cont = true; + + opt = PySequence_Fast(opt, "expected a sequence"); + if (!opt) { + PyErr_SetString(PyExc_TypeError, "'structures' must be a a sequence"); + return 1; + } + + structures_size = PySequence_Fast_GET_SIZE(opt); + // Getting space on the heap and do not forget to set self->numcolors. + structures = self->structures = calloc(structures_size, sizeof(struct Color)); + self->numcolors = structures_size; + if (structures == NULL) { + PyErr_SetString(PyExc_MemoryError, "failed to allocate memory"); + return 1; + } + + /** + * Try to parse the definitions of conditions and colors. + **/ + if (cont) { + for (i = 0; i < structures_size; i++) { + PyObject *structure = PyList_GET_ITEM(opt, i); + // condspy holding the conditions tuple of variable length (python object) + PyObject *condspy; + // colorpy holding the 4 tuple with r g b a values of the color + PyObject *colorpy; + + // getting the condspy and colorpy out of the structures. + if (!PyArg_ParseTuple(structure, "OO", &condspy, &colorpy)) { + // Exception set automatically + free(structures); + self->structures = NULL; + return 1; + } + + // Parse colorpy into a c-struct. + if (!PyArg_ParseTuple( colorpy, "bbbb", + &structures[i].r, + &structures[i].g, + &structures[i].b, + &structures[i].a)) { + free(structures); + self->structures = NULL; + return 1; + } + + // Convert condspy to a fast sequence + condspy = PySequence_Fast(condspy, "Failed to parse conditions"); + if(condspy == NULL) { + free(structures); + self->structures = NULL; + return 1; + } + + // get the number of conditions. + structures[i].numconds = PySequence_Fast_GET_SIZE(condspy); + // reserve enough memory for the conditions. + cond = calloc(structures[i].numconds, sizeof(struct Condition)); + structures[i].conditions = cond; + + if (structures[i].conditions == NULL) { + PyErr_SetString(PyExc_MemoryError, "failed to allocate memory"); + free(structures); + self->structures = NULL; + return 1; + } + + // iterate over all the conditions and read them. + for (n = 0; n < structures[i].numconds; n++) { + PyObject *ccond = PySequence_Fast_GET_ITEM(condspy, n); + if(!PyArg_ParseTuple( ccond, "iiib", + &cond[n].relx, + &cond[n].rely, + &cond[n].relz, + &cond[n].block)){ + int x = 0; + for(x = 0; x < structures_size; x++){ + free(structures[x].conditions); + } + free(structures); + self->structures = NULL; + return 1; + } + } + } + } + } + + /* setup custom color */ + self->parent.get_color = get_color; + + return 0; +} + +static void overlay_structure_finish(void *data, RenderState *state) { + /* first free all *our* stuff */ + RenderPrimitiveStructure* self = (RenderPrimitiveStructure *)data; + int i = 0; + + if(self->structures) { + // freeing the nested structure + struct Color * m = self->structures; + for(i = 0; i < self->numcolors; i++){ + if(m[i].conditions) + free(m[i].conditions); + } + } + + if (self->structures) { + free(self->structures); + self->structures = NULL; + } + + /* now, chain up */ + primitive_overlay.finish(data, state); +} + +RenderPrimitiveInterface primitive_overlay_structure = { + "overlay-structure", + sizeof(RenderPrimitiveStructure), + overlay_structure_start, + overlay_structure_finish, + NULL, + NULL, + overlay_draw, +}; + diff --git a/overviewer_core/textures.py b/overviewer_core/textures.py index 36ce0fd..144d762 100644 --- a/overviewer_core/textures.py +++ b/overviewer_core/textures.py @@ -3437,7 +3437,7 @@ def huge_mushroom(self, blockid, data): img = self.build_full_block(cap, None, None, cap, porous) if data == 2: # east side - img = self.build_full_block(cap, None, None, porous, porous) + img = self.build_full_block(cap, None, None, porous, cap) if data == 3: # south-east corner img = self.build_full_block(cap, None, None, porous, cap) diff --git a/overviewer_core/tileset.py b/overviewer_core/tileset.py index b744d6e..5cf56f1 100644 --- a/overviewer_core/tileset.py +++ b/overviewer_core/tileset.py @@ -689,6 +689,25 @@ class TileSet(object): # 2 is now 2/1 # 3 is now 3/0 # then all that needs to be done is to regenerate the new top level + + def rollback_mkdir(dnum): + p = getpath("new" + str(dnum)) + if os.path.exists(p): + os.rmdir(p) + + def rollback_filerename(dnum): + newnum = (3,2,1,0)[dnum] + qimg = getpath("new%d/%d.%s" % (dnum, newnum, self.imgextension)) + qdir = getpath("new%d/%d" % (dnum, newnum)) + + if os.path.exists(qimg): + os.rename(qimg, getpath("%d.%s" % (dnum, self.imgextension))) + if os.path.exists(qdir): + os.rename(qdir, getpath(str(dnum))) + + def rollback_dirrename(dnum): + os.rename(getpath(str(dnum)), getpath("new" + str(dnum))) + for dirnum in range(4): newnum = (3,2,1,0)[dirnum] @@ -698,12 +717,31 @@ class TileSet(object): files = [str(dirnum)+"."+self.imgextension, str(dirnum)] newfiles = [str(newnum)+"."+self.imgextension, str(newnum)] - os.mkdir(newdirpath) - for f, newf in zip(files, newfiles): - p = getpath(f) - if os.path.exists(p): - os.rename(p, getpath(newdir, newf)) - os.rename(newdirpath, getpath(str(dirnum))) + try: + try: + os.mkdir(newdirpath) + try: + for f, newf in zip(files, newfiles): + p = getpath(f) + if os.path.exists(p): + os.rename(p, getpath(newdir, newf)) + except: + rollback_filerename(dirnum) + raise + except: + rollback_mkdir(dirnum) + raise + os.rename(newdirpath, getpath(str(dirnum))) + except: + logging.warning("Overviewer was interrupted during tile " + "re-arrangement.") + logging.warning("Rolling back changes...") + # Moonwalk the fuck out of here + for lastdir in range(dirnum - 1, -1, -1): + rollback_dirrename(lastdir) + rollback_filerename(lastdir) + rollback_mkdir(lastdir) + raise def _decrease_depth(self): """If the map size decreases, or perhaps the user has a depth override diff --git a/overviewer_core/world.py b/overviewer_core/world.py index 0597317..910362f 100644 --- a/overviewer_core/world.py +++ b/overviewer_core/world.py @@ -105,8 +105,12 @@ class World(object): # Hard-code this to only work with format version 19133, "Anvil" if not ('version' in data and data['version'] == 19133): - logging.critical("Sorry, This version of Minecraft-Overviewer only works with the 'Anvil' chunk format") - raise ValueError("World at %s is not compatible with Overviewer" % self.worlddir) + if 'version' in data and data['version'] == 0: + logging.debug("Note: Allowing a version of zero in level.dat!") + ## XXX temporary fix for #1194 + else: + logging.critical("Sorry, This version of Minecraft-Overviewer only works with the 'Anvil' chunk format") + raise ValueError("World at %s is not compatible with Overviewer" % self.worlddir) # This isn't much data, around 15 keys and values for vanilla worlds. self.leveldat = data