diff --git a/MANIFEST.in b/MANIFEST.in index ecc747a..b5f518e 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -6,4 +6,5 @@ include sample.settings.py recursive-include contrib/ *.py recursive-include overviewer_core/ *.py recursive-include overviewer_core/src/ *.c *.h +recursive-include overviewer_core/src/primitives/ *.c *.h recursive-include overviewer_core/data/ * diff --git a/docs/config.rst b/docs/config.rst index d6d26bd..5b073dd 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -430,6 +430,26 @@ SmoothLighting (same as Lighting) +ClearBase + Forces the background to be transparent. Use this in place of Base + for rendering pure overlays. + +SpawnOverlay + Color the map red in areas where monsters can spawn. Either use + this on top of other modes, or on top of ClearBase to create a + pure overlay. + +MineralOverlay + Color the map according to what minerals can be found + underneath. Either use this on top of other modes, or on top of + ClearBase to create a pure overlay. + + **Options** + + minerals + A list of (blockid, (r, g, b)) tuples to use as colors. If not + provided, a default list of common minerals is used. + Defining Custom Rendermodes --------------------------- Each rendermode primitive listed above is a Python *class* that is automatically diff --git a/overviewer.py b/overviewer.py index 9ab4019..83ee257 100755 --- a/overviewer.py +++ b/overviewer.py @@ -365,8 +365,11 @@ dir but you forgot to put quotes around the directory, since it contains spaces. 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'], render_name) return 1 + + # If this is to be a rotated regionset, wrap it in a RotatedRegionSet + # object if (render['northdirection'] > 0): - rset = rset.rotate(render['northdirection']) + rset = world.RotatedRegionSet(rset, render['northdirection']) logging.debug("Using RegionSet %r", rset) # create our TileSet from this RegionSet @@ -380,6 +383,12 @@ dir but you forgot to put quotes around the directory, since it contains spaces. tset = tileset.TileSet(rset, assetMrg, tex, tileSetOpts, tileset_dir) tilesets.append(tset) + # Do tileset preprocessing here, before we start dispatching jobs + for ts in tilesets: + ts.do_preprocessing() + + # Output initial static data and configuration + assetMrg.initialize(tilesets) # multiprocessing dispatcher if config['processes'] == 1: diff --git a/overviewer_core/assetmanager.py b/overviewer_core/assetmanager.py index 5d60cae..26734b4 100644 --- a/overviewer_core/assetmanager.py +++ b/overviewer_core/assetmanager.py @@ -72,7 +72,23 @@ directory. # TODO based on the type, so something POI[regionset.name].append - def finalize(self, tilesets): + def initialize(self, tilesets): + """Similar to finalize() but calls the tilesets' get_initial_data() + instead of get_persistent_data() to compile the generated javascript + config. + + """ + return self.finalize(tilesets, True) + + def finalize(self, tilesets, initial=False): + """Called to output the generated javascript and all static files to + the output directory + + """ + if not initial: + get_data = lambda tileset: tileset.get_persistent_data() + else: + get_data = lambda tileset: tileset.get_initial_data() # dictionary to hold the overviewerConfig.js settings that we will dumps dump = dict() @@ -94,7 +110,7 @@ directory. # based on the tilesets we have, group them by worlds worlds = [] for tileset in tilesets: - full_name = tileset.get_persistent_data()['world'] + full_name = get_data(tileset)['world'] if full_name not in worlds: worlds.append(full_name) @@ -120,7 +136,7 @@ directory. for tileset in tilesets: - dump['tilesets'].append(tileset.get_persistent_data()) + dump['tilesets'].append(get_data(tileset)) # write a blank image blank = Image.new("RGBA", (1,1), tileset.options.get('bgcolor')) @@ -128,11 +144,10 @@ directory. jsondump = json.dumps(dump, indent=4) - with codecs.open(os.path.join(self.outputdir, 'overviewerConfig.js'), 'w', encoding='UTF-8') as f: - f.write("var overviewerConfig = " + jsondump + ";\n") + with util.FileReplacer(os.path.join(self.outputdir, "overviewerConfig.js")) as tmpfile: + with codecs.open(tmpfile, 'w', encoding='UTF-8') as f: + f.write("var overviewerConfig = " + jsondump + ";\n") - - # copy web assets into destdir: global_assets = os.path.join(util.get_program_path(), "overviewer_core", "data", "web_assets") if not os.path.isdir(global_assets): @@ -143,23 +158,16 @@ directory. js_src = os.path.join(util.get_program_path(), "overviewer_core", "data", "js_src") if not os.path.isdir(js_src): js_src = os.path.join(util.get_program_path(), "js_src") - with open(os.path.join(self.outputdir, "overviewer.js"), "w") as fout: - # first copy in js_src/overviewer.js - with open(os.path.join(js_src, "overviewer.js")) as f: - fout.write(f.read()) - # now copy in the rest - for js in os.listdir(js_src): - if not js.endswith("overviewer.js"): - with open(os.path.join(js_src,js)) as f: - fout.write(f.read()) - - # do the same with the local copy, if we have it - # TODO - # if self.web_assets_path: - # util.mirror_dir(self.web_assets_path, self.outputdir) - - - + with util.FileReplacer(os.path.join(self.outputdir, "overviewer.js")) as tmpfile: + with open(tmpfile, "w") as fout: + # first copy in js_src/overviewer.js + with open(os.path.join(js_src, "overviewer.js"), 'r') as f: + fout.write(f.read()) + # now copy in the rest + for js in os.listdir(js_src): + if not js.endswith("overviewer.js") and js.endswith(".js"): + with open(os.path.join(js_src,js)) as f: + fout.write(f.read()) # helper function to get a label for the given rendermode def get_render_mode_label(rendermode): info = get_render_mode_info(rendermode) @@ -177,7 +185,6 @@ directory. versionstr = "%s (%s)" % (overviewer_version.VERSION, overviewer_version.HASH[:7]) index = index.replace("{version}", versionstr) - with codecs.open(os.path.join(self.outputdir, "index.html"), 'w', encoding='UTF-8') as output: - output.write(index) - - + with util.FileReplacer(indexpath) as indexpath: + with codecs.open(indexpath, 'w', encoding='UTF-8') as output: + output.write(index) diff --git a/overviewer_core/data/js_src/views.js b/overviewer_core/data/js_src/views.js index bde253e..70867e0 100644 --- a/overviewer_core/data/js_src/views.js +++ b/overviewer_core/data/js_src/views.js @@ -36,21 +36,23 @@ overviewer.views.WorldView = Backbone.View.extend({ overviewer.views.WorldSelectorView = Backbone.View.extend({ initialize: function() { - // a div will have already been created for us, we just - // need to register it with the google maps control - var selectBox = document.createElement('select'); - $.each(overviewer.collections.worldViews, function(index, elem) { - var o = document.createElement("option"); - o.value = elem.model.get("name"); - o.innerHTML = elem.model.get("name"); - $(o).data("viewObj", elem); - selectBox.appendChild(o); + if(overviewer.collections.worldViews.length > 1) { + // a div will have already been created for us, we just + // need to register it with the google maps control + var selectBox = document.createElement('select'); + $.each(overviewer.collections.worldViews, function(index, elem) { + var o = document.createElement("option"); + o.value = elem.model.get("name"); + o.innerHTML = elem.model.get("name"); + $(o).data("viewObj", elem); + selectBox.appendChild(o); - }); + }); - this.el.appendChild(selectBox); - overviewer.map.controls[google.maps.ControlPosition.TOP_LEFT].push(this.el); - }, + this.el.appendChild(selectBox); + overviewer.map.controls[google.maps.ControlPosition.TOP_LEFT].push(this.el); + } + }, events: { "change select": "changeWorld" }, diff --git a/overviewer_core/dispatcher.py b/overviewer_core/dispatcher.py index 2e8e353..9823eef 100644 --- a/overviewer_core/dispatcher.py +++ b/overviewer_core/dispatcher.py @@ -49,14 +49,9 @@ class Dispatcher(object): """ # TODO use status callback - # preprocessing - for tileset in tilesetlist: - tileset.do_preprocessing() - # setup tilesetlist self.setup_tilesets(tilesetlist) - # iterate through all possible phases num_phases = [tileset.get_num_phases() for tileset in tilesetlist] for phase in xrange(max(num_phases)): diff --git a/overviewer_core/rendermodes.py b/overviewer_core/rendermodes.py index 12ab93b..6548cb9 100644 --- a/overviewer_core/rendermodes.py +++ b/overviewer_core/rendermodes.py @@ -146,6 +146,45 @@ class Lighting(RenderPrimitive): class SmoothLighting(Lighting): name = "smooth-lighting" +class ClearBase(RenderPrimitive): + name = "clear-base" + +class Overlay(RenderPrimitive): + name = "overlay" + + @property + def whitecolor(self): + whitecolor = getattr(self, "_whitecolor", None) + if whitecolor: + return whitecolor + white = Image.new("RGBA", (24,24), (255, 255, 255, 255)) + self._whitecolor = white + return white + + @property + def facemask_top(self): + facemask_top = getattr(self, "_facemask_top", None) + if facemask_top: + return facemask_top + + white = Image.new("L", (24,24), 255) + top = Image.new("L", (24,24), 0) + toppart = textures.Textures.transform_image_top(white) + top.paste(toppart, (0,0)) + for x,y in [(3,4), (7,2), (11,0)]: + top.putpixel((x,y), 255) + self._facemask_top = top + return top + +class SpawnOverlay(Overlay): + name = "overlay-spawn" + +class MineralOverlay(Overlay): + name = "overlay-mineral" + options = { + 'minerals' : ('a list of (blockid, (r, g, b)) tuples for coloring minerals', None), + } + # Built-in rendermodes for your convenience! normal = [Base(), EdgeLines()] lighting = [Base(), EdgeLines(), Lighting()] diff --git a/overviewer_core/settingsDefinition.py b/overviewer_core/settingsDefinition.py index 91d8560..4c3dd02 100644 --- a/overviewer_core/settingsDefinition.py +++ b/overviewer_core/settingsDefinition.py @@ -71,7 +71,7 @@ renders = Setting(required=True, default={}, "optimizeimg": Setting(required=True, validator=validateOptImg, default=0), "nomarkers": Setting(required=False, validator=validateBool, default=None), "texturepath": Setting(required=False, validator=validateTexturePath, default=None), - "renderchecks": Setting(required=True, validator=validateInt, default=0), + "renderchecks": Setting(required=False, validator=validateInt, default=None), "rerenderprob": Setting(required=True, validator=validateFloat, default=0), # Remove this eventually (once people update their configs) diff --git a/overviewer_core/signals.py b/overviewer_core/signals.py index 3e8dc5e..ca2eab8 100755 --- a/overviewer_core/signals.py +++ b/overviewer_core/signals.py @@ -89,3 +89,12 @@ class Signal(object): # convenience def __call__(self, *args, **kwargs): self.emit(*args, **kwargs) + + # force pickled signals to redirect to existing signals + def __getstate__(self): + return self.fullname + def __setstate__(self, fullname): + for attr in dir(self.signals[fullname]): + if attr.startswith('_'): + continue + setattr(self, attr, getattr(self.signals[fullname], attr)) diff --git a/overviewer_core/src/overviewer.h b/overviewer_core/src/overviewer.h index d402795..89b3c6e 100644 --- a/overviewer_core/src/overviewer.h +++ b/overviewer_core/src/overviewer.h @@ -26,7 +26,7 @@ // increment this value if you've made a change to the c extesion // and want to force users to rebuild -#define OVERVIEWER_EXTENSION_VERSION 25 +#define OVERVIEWER_EXTENSION_VERSION 26 /* Python PIL, and numpy headers */ #include diff --git a/overviewer_core/src/primitives/clear-base.c b/overviewer_core/src/primitives/clear-base.c new file mode 100644 index 0000000..3ee36dc --- /dev/null +++ b/overviewer_core/src/primitives/clear-base.c @@ -0,0 +1,49 @@ +/* + * 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 . + */ + +#include "../overviewer.h" + +static int +clear_base_occluded(void *data, RenderState *state, int x, int y, int z) { + if ( (x != 0) && (y != 15) && (z != 127) && + !render_mode_hidden(state->rendermode, x-1, y, z) && + !render_mode_hidden(state->rendermode, x, y, z+1) && + !render_mode_hidden(state->rendermode, x, y+1, z) && + !is_transparent(getArrayByte3D(state->blocks, x-1, y, z)) && + !is_transparent(getArrayByte3D(state->blocks, x, y, z+1)) && + !is_transparent(getArrayByte3D(state->blocks, x, y+1, z))) { + return 1; + } + + return 0; +} + +static void +clear_base_draw(void *data, RenderState *state, PyObject *src, PyObject *mask, PyObject *mask_light) { + /* clear the draw space -- set alpha to 0 within mask */ + tint_with_mask(state->img, 255, 255, 255, 0, mask, state->imgx, state->imgy, 0, 0); +} + +RenderPrimitiveInterface primitive_clear_base = { + "clear-base", + 0, + NULL, + NULL, + clear_base_occluded, + NULL, + clear_base_draw, +}; diff --git a/overviewer_core/src/rendermode-mineral.c b/overviewer_core/src/primitives/overlay-mineral.c similarity index 64% rename from overviewer_core/src/rendermode-mineral.c rename to overviewer_core/src/primitives/overlay-mineral.c index 858333b..5d6f22e 100644 --- a/overviewer_core/src/rendermode-mineral.c +++ b/overviewer_core/src/primitives/overlay-mineral.c @@ -15,7 +15,14 @@ * with the Overviewer. If not, see . */ -#include "overviewer.h" +#include "overlay.h" + +typedef struct { + /* inherits from overlay */ + RenderPrimitiveOverlay parent; + + void *minerals; +} RenderPrimitiveMineral; struct MineralColor { unsigned char blockid; @@ -43,15 +50,16 @@ static struct MineralColor default_minerals[] = { static void get_color(void *data, RenderState *state, unsigned char *r, unsigned char *g, unsigned char *b, unsigned char *a) { - int x = state->x, y = state->y, z_max = state->z + 1, z; + int x = state->x, z = state->z, y_max, y; int max_i = -1; - RenderModeMineral* self = (RenderModeMineral *)data; + RenderPrimitiveMineral* self = (RenderPrimitiveMineral *)data; struct MineralColor *minerals = (struct MineralColor *)(self->minerals); *a = 0; - for (z = 0; z <= z_max; z++) { + y_max = state->y + 1; + for (y = state->chunky * -16; y <= y_max; y++) { int i, tmp; - unsigned char blockid = getArrayByte3D(state->blocks, x, y, z); + unsigned short blockid = get_data(state, BLOCKS, x, y, z); for (i = 0; (max_i == -1 || i < max_i) && minerals[i].blockid != 0; i++) { if (minerals[i].blockid == blockid) { @@ -59,7 +67,7 @@ static void get_color(void *data, RenderState *state, *g = minerals[i].g; *b = minerals[i].b; - tmp = (128 - z_max + z) * 2 - 40; + tmp = (128 - y_max + y) * 2 - 40; *a = MIN(MAX(0, tmp), 255); max_i = i; @@ -70,20 +78,21 @@ static void get_color(void *data, RenderState *state, } static int -rendermode_mineral_start(void *data, RenderState *state, PyObject *options) { +overlay_mineral_start(void *data, RenderState *state, PyObject *support) { PyObject *opt; - RenderModeMineral* self; + RenderPrimitiveMineral* self; /* first, chain up */ - int ret = rendermode_overlay.start(data, state, options); + int ret = primitive_overlay.start(data, state, support); if (ret != 0) return ret; /* now do custom initializations */ - self = (RenderModeMineral *)data; + self = (RenderPrimitiveMineral *)data; - opt = PyDict_GetItemString(options, "minerals"); - if (opt) { + if (!render_mode_parse_option(support, "minerals", "O", &(opt))) + return 1; + if (opt && opt != Py_None) { struct MineralColor *minerals = NULL; Py_ssize_t minerals_size = 0, i; /* create custom minerals */ @@ -110,6 +119,7 @@ rendermode_mineral_start(void *data, RenderState *state, PyObject *options) { } else { self->minerals = default_minerals; } + Py_XDECREF(opt); /* setup custom color */ self->parent.get_color = get_color; @@ -118,50 +128,24 @@ rendermode_mineral_start(void *data, RenderState *state, PyObject *options) { } static void -rendermode_mineral_finish(void *data, RenderState *state) { +overlay_mineral_finish(void *data, RenderState *state) { /* first free all *our* stuff */ - RenderModeMineral* self = (RenderModeMineral *)data; + RenderPrimitiveMineral* self = (RenderPrimitiveMineral *)data; if (self->minerals && self->minerals != default_minerals) { free(self->minerals); } /* now, chain up */ - rendermode_overlay.finish(data, state); + primitive_overlay.finish(data, state); } -static int -rendermode_mineral_occluded(void *data, RenderState *state, int x, int y, int z) { - /* no special occlusion here */ - return rendermode_overlay.occluded(data, state, x, y, z); -} - -static int -rendermode_mineral_hidden(void *data, RenderState *state, int x, int y, int z) { - /* no special hiding here */ - return rendermode_overlay.hidden(data, state, x, y, z); -} - -static void -rendermode_mineral_draw(void *data, RenderState *state, PyObject *src, PyObject *mask, PyObject *mask_light) { - /* draw normally */ - rendermode_overlay.draw(data, state, src, mask, mask_light); -} - -const RenderModeOption rendermode_mineral_options[] = { - {"minerals", "a list of (blockid, (r, g, b)) tuples for coloring minerals"}, - {NULL, NULL} -}; - -RenderModeInterface rendermode_mineral = { - "mineral", "Mineral", - "draws a colored overlay showing where ores are located", - rendermode_mineral_options, - &rendermode_overlay, - sizeof(RenderModeMineral), - rendermode_mineral_start, - rendermode_mineral_finish, - rendermode_mineral_occluded, - rendermode_mineral_hidden, - rendermode_mineral_draw, +RenderPrimitiveInterface primitive_overlay_mineral = { + "overlay-mineral", + sizeof(RenderPrimitiveMineral), + overlay_mineral_start, + overlay_mineral_finish, + NULL, + NULL, + overlay_draw, }; diff --git a/overviewer_core/src/primitives/overlay-spawn.c b/overviewer_core/src/primitives/overlay-spawn.c new file mode 100644 index 0000000..872dc0f --- /dev/null +++ b/overviewer_core/src/primitives/overlay-spawn.c @@ -0,0 +1,92 @@ +/* + * 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 . + */ + +#include "overlay.h" +#include + +typedef struct { + /* inherits from overlay */ + RenderPrimitiveOverlay parent; +} RenderPrimitiveSpawn; + +static void get_color(void *data, RenderState *state, + unsigned char *r, unsigned char *g, unsigned char *b, unsigned char *a) { + + RenderPrimitiveSpawn* self = (RenderPrimitiveSpawn *)data; + int x = state->x, y = state->y, z = state->z; + int y_light = y + 1; + unsigned char blocklight, skylight; + PyObject *block_py; + + /* set a nice, pretty red color */ + *r = 229; + *g = 36; + *b = 38; + + /* default to no overlay, until told otherwise */ + *a = 0; + + if (block_has_property(state->block, NOSPAWN)) { + /* nothing can spawn on this */ + return; + } + + blocklight = get_data(state, BLOCKLIGHT, x, y_light, z); + skylight = get_data(state, SKYLIGHT, x, y_light, z); + + if (MAX(blocklight, skylight) <= 7) { + /* hostile mobs spawn in daylight */ + *a = 240; + } else if (MAX(blocklight, skylight - 11) <= 7) { + /* hostile mobs spawn at night */ + *a = 150; + } +} + +static int +overlay_spawn_start(void *data, RenderState *state, PyObject *support) { + RenderPrimitiveSpawn* self; + + /* first, chain up */ + int ret = primitive_overlay.start(data, state, support); + if (ret != 0) + return ret; + + /* now do custom initializations */ + self = (RenderPrimitiveSpawn *)data; + self->parent.get_color = get_color; + + return 0; +} + +static void +overlay_spawn_finish(void *data, RenderState *state) { + RenderPrimitiveSpawn* self = (RenderPrimitiveSpawn *)data; + + /* chain up */ + primitive_overlay.finish(data, state); +} + +RenderPrimitiveInterface primitive_overlay_spawn = { + "overlay-spawn", + sizeof(RenderPrimitiveSpawn), + overlay_spawn_start, + overlay_spawn_finish, + NULL, + NULL, + overlay_draw, +}; diff --git a/overviewer_core/src/primitives/overlay.c b/overviewer_core/src/primitives/overlay.c new file mode 100644 index 0000000..6c1215b --- /dev/null +++ b/overviewer_core/src/primitives/overlay.c @@ -0,0 +1,99 @@ +/* + * 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 . + */ + +#include "overlay.h" + +static void get_color(void *data, RenderState *state, + unsigned char *r, unsigned char *g, unsigned char *b, unsigned char *a) { + *r = 200; + *g = 200; + *b = 255; + *a = 155; +} + +static int +overlay_start(void *data, RenderState *state, PyObject *support) { + PyObject *facemasks_py; + RenderPrimitiveOverlay *self = (RenderPrimitiveOverlay *)data; + + self->facemask_top = PyObject_GetAttrString(support, "facemask_top"); + self->white_color = PyObject_GetAttrString(support, "whitecolor"); + self->get_color = get_color; + + return 0; +} + +static void +overlay_finish(void *data, RenderState *state) { + RenderPrimitiveOverlay *self = (RenderPrimitiveOverlay *)data; + + Py_DECREF(self->facemask_top); + Py_DECREF(self->white_color); +} + +void +overlay_draw(void *data, RenderState *state, PyObject *src, PyObject *mask, PyObject *mask_light) { + RenderPrimitiveOverlay *self = (RenderPrimitiveOverlay *)data; + unsigned char r, g, b, a; + unsigned short top_block; + + // exactly analogous to edge-line code for these special blocks + int increment=0; + if (state->block == 44) // half-step + increment=6; + else if (state->block == 78) // snow + increment=9; + + /* skip rendering the overlay if we can't see it */ + top_block = get_data(state, BLOCKS, state->x, state->y+1, state->z); + if (!is_transparent(top_block)) { + return; + } + + /* check to be sure this block is solid/fluid */ + if (block_has_property(top_block, SOLID) || block_has_property(top_block, FLUID)) { + + /* top block is fluid or solid, skip drawing */ + return; + } + + /* check to be sure this block is solid/fluid */ + if (!block_has_property(state->block, SOLID) && !block_has_property(state->block, FLUID)) { + + /* not fluid or solid, skip drawing the overlay */ + return; + } + + /* get our color info */ + self->get_color(data, state, &r, &g, &b, &a); + + /* do the overlay */ + if (a > 0) { + alpha_over(state->img, self->white_color, self->facemask_top, state->imgx, state->imgy + increment, 0, 0); + tint_with_mask(state->img, r, g, b, a, self->facemask_top, state->imgx, state->imgy + increment, 0, 0); + } +} + +RenderPrimitiveInterface primitive_overlay = { + "overlay", + sizeof(RenderPrimitiveOverlay), + overlay_start, + overlay_finish, + NULL, + NULL, + overlay_draw, +}; diff --git a/overviewer_core/src/primitives/overlay.h b/overviewer_core/src/primitives/overlay.h new file mode 100644 index 0000000..cc028bb --- /dev/null +++ b/overviewer_core/src/primitives/overlay.h @@ -0,0 +1,31 @@ +/* + * 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 . + */ + +#include "../overviewer.h" + +typedef struct { + /* top facemask and white color image, for drawing overlays */ + PyObject *facemask_top, *white_color; + /* can be overridden in derived classes to control + overlay alpha and color + last four vars are r, g, b, a out */ + void (*get_color)(void *, RenderState *, + unsigned char *, unsigned char *, unsigned char *, unsigned char *); +} RenderPrimitiveOverlay; +extern RenderPrimitiveInterface primitive_overlay; + +void overlay_draw(void *data, RenderState *state, PyObject *src, PyObject *mask, PyObject *mask_light); diff --git a/overviewer_core/src/rendermode-overlay.c b/overviewer_core/src/rendermode-overlay.c deleted file mode 100644 index 9ae4e25..0000000 --- a/overviewer_core/src/rendermode-overlay.c +++ /dev/null @@ -1,132 +0,0 @@ -/* - * This file is part of the Minecraft Overviewer. - * - * Minecraft Overviewer is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as published - * by the Free Software Foundation, either version 3 of the License, or (at - * your option) any later version. - * - * Minecraft Overviewer is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General - * Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with the Overviewer. If not, see . - */ - -#include "overviewer.h" - -static void get_color(void *data, RenderState *state, - unsigned char *r, unsigned char *g, unsigned char *b, unsigned char *a) { - *r = 200; - *g = 200; - *b = 255; - *a = 155; -} - -static int -rendermode_overlay_start(void *data, RenderState *state, PyObject *options) { - PyObject *facemasks_py; - RenderModeOverlay *self = (RenderModeOverlay *)data; - - facemasks_py = PyObject_GetAttrString(state->support, "facemasks"); - /* borrowed reference, needs to be incref'd if we keep it */ - self->facemask_top = PyTuple_GetItem(facemasks_py, 0); - Py_INCREF(self->facemask_top); - Py_DECREF(facemasks_py); - - self->white_color = PyObject_GetAttrString(state->support, "white_color"); - self->get_color = get_color; - - return 0; -} - -static void -rendermode_overlay_finish(void *data, RenderState *state) { - RenderModeOverlay *self = (RenderModeOverlay *)data; - - Py_DECREF(self->facemask_top); - Py_DECREF(self->white_color); -} - -static int -rendermode_overlay_occluded(void *data, RenderState *state, int x, int y, int z) { - if ( (x != 0) && (y != 15) && (z != 127) && - !render_mode_hidden(state->rendermode, x-1, y, z) && - !render_mode_hidden(state->rendermode, x, y, z+1) && - !render_mode_hidden(state->rendermode, x, y+1, z) && - !is_transparent(getArrayByte3D(state->blocks, x-1, y, z)) && - !is_transparent(getArrayByte3D(state->blocks, x, y, z+1)) && - !is_transparent(getArrayByte3D(state->blocks, x, y+1, z))) { - return 1; - } - - return 0; -} - -static int -rendermode_overlay_hidden(void *data, RenderState *state, int x, int y, int z) { - /* overlays hide nothing by default */ - return 0; -} - -static void -rendermode_overlay_draw(void *data, RenderState *state, PyObject *src, PyObject *mask, PyObject *mask_light) { - RenderModeOverlay *self = (RenderModeOverlay *)data; - unsigned char r, g, b, a; - - // exactly analogous to edge-line code for these special blocks - int increment=0; - if (state->block == 44) // half-step - increment=6; - else if (state->block == 78) // snow - increment=9; - - /* clear the draw space -- set alpha to 0 within mask */ - tint_with_mask(state->img, 255, 255, 255, 0, mask, state->imgx, state->imgy, 0, 0); - - /* skip rendering the overlay if we can't see it */ - if (state->z != 127) { - unsigned char top_block = getArrayByte3D(state->blocks, state->x, state->y, state->z+1); - if (!is_transparent(top_block)) { - return; - } - - /* check to be sure this block is solid/fluid */ - if (block_has_property(top_block, SOLID) || block_has_property(top_block, FLUID)) { - - /* top block is fluid or solid, skip drawing */ - return; - } - } - - /* check to be sure this block is solid/fluid */ - if (!block_has_property(state->block, SOLID) && !block_has_property(state->block, FLUID)) { - - /* not fluid or solid, skip drawing the overlay */ - return; - } - - /* get our color info */ - self->get_color(data, state, &r, &g, &b, &a); - - /* do the overlay */ - if (a > 0) { - alpha_over(state->img, self->white_color, self->facemask_top, state->imgx, state->imgy + increment, 0, 0); - tint_with_mask(state->img, r, g, b, a, self->facemask_top, state->imgx, state->imgy + increment, 0, 0); - } -} - -RenderModeInterface rendermode_overlay = { - "overlay", "Overlay", - "base rendermode for informational overlays", - NULL, - NULL, - sizeof(RenderModeOverlay), - rendermode_overlay_start, - rendermode_overlay_finish, - rendermode_overlay_occluded, - rendermode_overlay_hidden, - rendermode_overlay_draw, -}; diff --git a/overviewer_core/src/rendermode-spawn.c b/overviewer_core/src/rendermode-spawn.c deleted file mode 100644 index 0c8027b..0000000 --- a/overviewer_core/src/rendermode-spawn.c +++ /dev/null @@ -1,122 +0,0 @@ -/* - * This file is part of the Minecraft Overviewer. - * - * Minecraft Overviewer is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as published - * by the Free Software Foundation, either version 3 of the License, or (at - * your option) any later version. - * - * Minecraft Overviewer is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General - * Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with the Overviewer. If not, see . - */ - -#include "overviewer.h" -#include - -static void get_color(void *data, RenderState *state, - unsigned char *r, unsigned char *g, unsigned char *b, unsigned char *a) { - - RenderModeSpawn* self = (RenderModeSpawn *)data; - int x = state->x, y = state->y, z = state->z; - int z_light = z + 1; - unsigned char blocklight, skylight; - PyObject *block_py; - - /* set a nice, pretty red color */ - *r = 229; - *g = 36; - *b = 38; - - /* default to no overlay, until told otherwise */ - *a = 0; - - if (block_has_property(state->block, NOSPAWN)) { - /* nothing can spawn on this */ - return; - } - - blocklight = getArrayByte3D(self->blocklight, x, y, MIN(127, z_light)); - - /* if we're at the top, force 15 (brightest!) skylight */ - if (z_light == 128) { - skylight = 15; - } else { - skylight = getArrayByte3D(self->skylight, x, y, z_light); - } - - if (MAX(blocklight, skylight) <= 7) { - /* hostile mobs spawn in daylight */ - *a = 240; - } else if (MAX(blocklight, skylight - 11) <= 7) { - /* hostile mobs spawn at night */ - *a = 150; - } -} - -static int -rendermode_spawn_start(void *data, RenderState *state, PyObject *options) { - RenderModeSpawn* self; - - /* first, chain up */ - int ret = rendermode_overlay.start(data, state, options); - if (ret != 0) - return ret; - - /* now do custom initializations */ - self = (RenderModeSpawn *)data; - self->blocklight = get_chunk_data(state, CURRENT, BLOCKLIGHT, 1); - self->skylight = get_chunk_data(state, CURRENT, SKYLIGHT, 1); - - /* setup custom color */ - self->parent.get_color = get_color; - - return 0; -} - -static void -rendermode_spawn_finish(void *data, RenderState *state) { - /* first free all *our* stuff */ - RenderModeSpawn* self = (RenderModeSpawn *)data; - - Py_DECREF(self->blocklight); - Py_DECREF(self->skylight); - - /* now, chain up */ - rendermode_overlay.finish(data, state); -} - -static int -rendermode_spawn_occluded(void *data, RenderState *state, int x, int y, int z) { - /* no special occlusion here */ - return rendermode_overlay.occluded(data, state, x, y, z); -} - -static int -rendermode_spawn_hidden(void *data, RenderState *state, int x, int y, int z) { - /* no special hiding here */ - return rendermode_overlay.hidden(data, state, x, y, z); -} - -static void -rendermode_spawn_draw(void *data, RenderState *state, PyObject *src, PyObject *mask, PyObject *mask_light) { - /* draw normally */ - rendermode_overlay.draw(data, state, src, mask, mask_light); -} - -RenderModeInterface rendermode_spawn = { - "spawn", "Spawn", - "draws a red overlay where monsters can spawn at night", - NULL, - &rendermode_overlay, - sizeof(RenderModeSpawn), - rendermode_spawn_start, - rendermode_spawn_finish, - rendermode_spawn_occluded, - rendermode_spawn_hidden, - rendermode_spawn_draw, -}; diff --git a/overviewer_core/src/rendermodes.h b/overviewer_core/src/rendermodes.h index 23df5e6..6408760 100644 --- a/overviewer_core/src/rendermodes.h +++ b/overviewer_core/src/rendermodes.h @@ -102,38 +102,4 @@ void render_mode_draw(RenderMode *self, PyObject *img, PyObject *mask, PyObject works like PyArg_ParseTuple on a support object */ int render_mode_parse_option(PyObject *support, const char *name, const char *format, ...); -/* XXX individual rendermode interface declarations follow */ -#ifdef OLD_MODES - -/* OVERLAY */ -typedef struct { - /* top facemask and white color image, for drawing overlays */ - PyObject *facemask_top, *white_color; - /* can be overridden in derived classes to control - overlay alpha and color - last four vars are r, g, b, a out */ - void (*get_color)(void *, RenderState *, - unsigned char *, unsigned char *, unsigned char *, unsigned char *); -} RenderModeOverlay; -extern RenderModeInterface rendermode_overlay; - -/* SPAWN */ -typedef struct { - /* inherits from overlay */ - RenderModeOverlay parent; - - PyObject *skylight, *blocklight; -} RenderModeSpawn; -extern RenderModeInterface rendermode_spawn; - -/* MINERAL */ -typedef struct { - /* inherits from overlay */ - RenderModeOverlay parent; - - void *minerals; -} RenderModeMineral; -extern RenderModeInterface rendermode_mineral; -#endif /* OLD_MODES */ - #endif /* __RENDERMODES_H_INCLUDED__ */ diff --git a/overviewer_core/tileset.py b/overviewer_core/tileset.py index 4a45a08..3bfd35d 100644 --- a/overviewer_core/tileset.py +++ b/overviewer_core/tileset.py @@ -29,6 +29,7 @@ from itertools import product, izip from PIL import Image from .util import iterate_base4, convert_coords, unconvert_coords, roundrobin +from .util import FileReplacer from .optimizeimages import optimize_image import c_overviewer @@ -189,7 +190,9 @@ class TileSet(object): renderchecks An integer indicating how to determine which tiles need updating - and which don't. This is one of three levels: + and which don't. This key is optional; if not specified, an + appropriate mode is determined from the persistent config obtained + from the asset manager. This is one of three levels: 0 Only render tiles that have chunks with a greater mtime than @@ -258,8 +261,38 @@ class TileSet(object): self.am = assetmanagerobj self.textures = texturesobj - self.last_rendertime = self.am.get_tileset_config(self.options.get("name")).get('last_rendertime', 0) - self.this_rendertime = time.time() + config = self.am.get_tileset_config(self.options.get("name")) + self.config = config + + self.last_rendertime = config.get('last_rendertime', 0) + + if "renderchecks" not in self.options: + if not config: + # No persistent config? This is a full render then. + self.options['renderchecks'] = 2 + logging.debug("This is the first time rendering %s. Doing" + + " a full-render", + self.options['name']) + elif config.get("render_in_progress", False): + # The last render must have been interrupted. The default should be + # 1 then, not 0 + logging.warning( + "The last render for %s didn't finish. I'll be " + + "scanning all the tiles to make sure everything's up "+ + "to date.", + self.options['name'], + ) + logging.warning("You won't get percentage progress for "+ + "this run only, because I don't know how many tiles "+ + "need rendering. I'll be checking them as I go") + self.options['renderchecks'] = 1 + else: + logging.debug("No rendercheck mode specified for %s. "+ + "Rendering tile whose chunks have changed since %s", + self.options['name'], + time.strftime("%x %X", time.localtime(self.last_rendertime)), + ) + self.options['renderchecks'] = 0 # Throughout the class, self.outputdir is an absolute path to the # directory where we output tiles. It is assumed to exist. @@ -298,8 +331,11 @@ class TileSet(object): logging.warning("Just letting you know, your map requries %s zoom levels. This is REALLY big!", self.treedepth) - # Do any tile re-arranging if necessary - self._rearrange_tiles() + # Do any tile re-arranging if necessary. Skip if there was no config + # from the asset-manager, which typically indicates this is a new + # render + if self.config: + self._rearrange_tiles() # Do the chunk scan here self.dirtytree = self._chunk_scan() @@ -383,6 +419,20 @@ class TileSet(object): name = str(tilepath[-1]) self._render_compositetile(dest, name) + def get_initial_data(self): + """This is called similarly to get_persistent_data, but is called after + do_preprocessing but before any work is acutally done. + + """ + d = self.get_persistent_data() + # This is basically the same as get_persistent_data() with the + # following exceptions: + # * last_rendertime is not changed + # * A key "render_in_progress" is set to True + d['last_rendertime'] = self.last_rendertime + d['render_in_progress'] = True + return d + def get_persistent_data(self): """Returns a dictionary representing the persistent data of this TileSet. Typically this is called by AssetManager @@ -400,7 +450,7 @@ class TileSet(object): bgcolor = bgcolorformat(self.options.get('bgcolor')), world = self.options.get('worldname_orig') + (" - " + self.options.get('dimension') if self.options.get('dimension') != 'default' else ''), - last_rendertime = self.this_rendertime, + last_rendertime = self.max_chunk_mtime, imgextension = self.imgextension, ) try: @@ -463,16 +513,17 @@ class TileSet(object): """ try: - curdepth = get_dirdepth(self.outputdir) - except Exception: - logging.critical("Could not determine existing tile tree depth. Does it exist?") - raise + curdepth = self.config['zoomLevels'] + except KeyError: + return if curdepth == 1: # Skip a depth 1 tree. A depth 1 tree pretty much can't happen, so # when we detect this it usually means the tree is actually empty return - logging.debug("Current tree depth was detected to be %s. Target tree depth is %s", curdepth, self.treedepth) + logging.debug("Current tree depth for %s is reportedly %s. Target tree depth is %s", + self.options['name'], + curdepth, self.treedepth) if self.treedepth != curdepth: if self.treedepth > curdepth: logging.warning("Your map seems to have expanded beyond its previous bounds.") @@ -648,8 +699,8 @@ class TileSet(object): dirty.add(tile.path) t = int(time.time()-stime) - logging.debug("%s finished chunk scan. %s chunks scanned in %s second%s", - self, chunkcount, t, + logging.debug("Finished chunk scan for %s. %s chunks scanned in %s second%s", + self.options['name'], chunkcount, t, "s" if t != 1 else "") self.max_chunk_mtime = max_chunk_mtime @@ -728,22 +779,23 @@ class TileSet(object): img.paste(quad, path[0]) except Exception, e: logging.warning("Couldn't open %s. It may be corrupt. Error was '%s'", path[1], e) - logging.warning("I'm going to try and delete it. You will need to run the render again") + logging.warning("I'm going to try and delete it. You will need to run the render again and with --check-tiles") try: os.unlink(path[1]) except Exception, e: logging.error("While attempting to delete corrupt image %s, an error was encountered. You will need to delete it yourself. Error was '%s'", path[1], e) # Save it - if imgformat == 'jpg': - img.save(imgpath, quality=self.options['imgquality'], subsampling=0) - else: # png - img.save(imgpath) - - if self.options['optimizeimg']: - optimize_image(imgpath, imgformat, self.options['optimizeimg']) + with FileReplacer(imgpath) as tmppath: + if imgformat == 'jpg': + img.save(tmppath, "jpeg", quality=self.options['imgquality'], subsampling=0) + else: # png + img.save(tmppath, "png") + + if self.options['optimizeimg']: + optimize_image(tmppath, imgformat, self.options['optimizeimg']) - os.utime(imgpath, (max_mtime, max_mtime)) + os.utime(tmppath, (max_mtime, max_mtime)) def _render_rendertile(self, tile): """Renders the given render-tile. @@ -829,15 +881,16 @@ class TileSet(object): #draw.text((96,96), "c,r: %s,%s" % (col, row), fill='red') # Save them - if self.imgextension == 'jpg': - tileimg.save(imgpath, quality=self.options['imgquality'], subsampling=0) - else: # png - tileimg.save(imgpath) + with FileReplacer(imgpath) as tmppath: + if self.imgextension == 'jpg': + tileimg.save(tmppath, "jpeg", quality=self.options['imgquality'], subsampling=0) + else: # png + tileimg.save(tmppath, "png") - if self.options['optimizeimg']: - optimize_image(imgpath, self.imgextension, self.options['optimizeimg']) + if self.options['optimizeimg']: + optimize_image(tmppath, self.imgextension, self.options['optimizeimg']) - os.utime(imgpath, (max_chunk_mtime, max_chunk_mtime)) + os.utime(tmppath, (max_chunk_mtime, max_chunk_mtime)) def _iterate_and_check_tiles(self, path): """A generator function over all tiles that need rendering in the @@ -946,34 +999,6 @@ class TileSet(object): # Nope. yield path, max_child_mtime, False - -def get_dirdepth(outputdir): - """Returns the current depth of the tree on disk - - """ - # Traverses down the first directory until it reaches one with no - # subdirectories. While all paths of the tree may not exist, all paths - # of the tree should and are assumed to be the same depth - - # This function returns a list of all subdirectories of the given - # directory. It's slightly more complicated than you'd think it should be - # because one must turn directory names returned by os.listdir into - # relative/absolute paths before they can be passed to os.path.isdir() - getsubdirs = lambda directory: [ - abssubdir - for abssubdir in - (os.path.join(directory,subdir) for subdir in os.listdir(directory)) - if os.path.isdir(abssubdir) - ] - - depth = 1 - subdirs = getsubdirs(outputdir) - while subdirs: - subdirs = getsubdirs(subdirs[0]) - depth += 1 - - return depth - ###################### # The following two functions define the mapping from chunks to tiles and back. # The mapping from chunks to tiles (get_tiles_by_chunk()) is used during the diff --git a/overviewer_core/util.py b/overviewer_core/util.py index 3661b2e..ded3f11 100644 --- a/overviewer_core/util.py +++ b/overviewer_core/util.py @@ -152,6 +152,74 @@ def unconvert_coords(col, row): # col - row = chunkx + chunkx => (col - row)/2 = chunkx return ((col - row) / 2, (col + row) / 2) +# Define a context manager to handle atomic renaming or "just forget it write +# straight to the file" depending on whether os.rename provides atomic +# overwrites. +# Detect whether os.rename will overwrite files +import tempfile +with tempfile.NamedTemporaryFile() as f1: + with tempfile.NamedTemporaryFile() as f2: + try: + os.rename(f1.name,f2.name) + except OSError: + renameworks = False + else: + renameworks = True + # re-make this file so it can be deleted without error + open(f1.name, 'w').close() +del tempfile,f1,f2 +doc = """This class acts as a context manager for files that are to be written +out overwriting an existing file. + +The parameter is the destination filename. The value returned into the context +is the filename that should be used. On systems that support an atomic +os.rename(), the filename will actually be a temporary file, and it will be +atomically replaced over the destination file on exit. + +On systems that don't support an atomic rename, the filename returned is the +filename given. + +If an error is encountered, the file is attempted to be removed, and the error +is propagated. + +Example: + +with FileReplacer("config") as configname: + with open(configout, 'w') as configout: + configout.write(newconfig) +""" +if renameworks: + class FileReplacer(object): + __doc__ = doc + def __init__(self, destname): + self.destname = destname + self.tmpname = destname + ".tmp" + def __enter__(self): + # rename works here. Return a temporary filename + return self.tmpname + def __exit__(self, exc_type, exc_val, exc_tb): + if exc_type: + # error + try: + os.remove(self.tmpname) + except Exception, e: + logging.warning("An error was raised, so I was doing " + "some cleanup first, but I couldn't remove " + "'%s'!", self.tmpname) + else: + # atomic rename into place + os.rename(self.tmpname, self.destname) +else: + class FileReplacer(object): + __doc__ = doc + def __init__(self, destname): + self.destname = destname + def __enter__(self): + return self.destname + def __exit__(self, exc_type, exc_val, exc_tb): + return +del renameworks + # Logging related classes are below # Some cool code for colored logging: diff --git a/overviewer_core/world.py b/overviewer_core/world.py index 1d50983..2ab2480 100644 --- a/overviewer_core/world.py +++ b/overviewer_core/world.py @@ -34,10 +34,10 @@ class ChunkDoesntExist(Exception): def log_other_exceptions(func): """A decorator that prints out any errors that are not - ChunkDoesntExist errors. This decorates get_chunk because the C - code is likely to swallow exceptions, so this will at least make - them visible. - + ChunkDoesntExist errors. This should decorate any functions or + methods called by the C code, such as get_chunk(), because the C + code is likely to swallow exceptions. This will at least make them + visible. """ functools.wraps(func) def newfunc(*args): @@ -359,9 +359,6 @@ class RegionSet(object): return chunk_data - def rotate(self, north_direction): - return RotatedRegionSet(self.regiondir, north_direction) - def iterate_chunks(self): """Returns an iterator over all chunk metadata in this world. Iterates over tuples of integers (x,z,mtime) for each chunk. Other chunk data @@ -412,6 +409,33 @@ class RegionSet(object): x = int(p[1]) y = int(p[2]) yield (x, y, path) + +class RegionSetWrapper(object): + """This is the base class for all "wrappers" of RegionSet objects. A + wrapper is an object that acts similarly to a subclass: some methods are + overridden and functionality is changed, others may not be. The difference + here is that these wrappers may wrap each other, forming chains. + + In fact, subclasses of this object may act exactly as if they've subclassed + the original RegionSet object, except the first parameter of the + constructor is a regionset object, not a regiondir. + + This class must implement the full public interface of RegionSet objects + + """ + def __init__(self, rsetobj): + self._r = rsetobj + + def get_type(self): + return self._r.get_type() + def get_biome_data(self, x, z): + return self._r.get_biome_data(x,z) + def get_chunk(self, x, z): + return self._r.get_chunk(x,z) + def iterate_chunks(self): + return self._r.iterate_chunks() + def get_chunk_mtime(self, x, z): + return self._r.get_chunk_mtime(x,z) # see RegionSet.rotate. These values are chosen so that they can be # passed directly to rot90; this means that they're the number of @@ -421,7 +445,7 @@ UPPER_RIGHT = 1 ## - Return the world such that north is down the +X axis (rotat LOWER_RIGHT = 2 ## - Return the world such that north is down the +Z axis (rotate 180 degrees) LOWER_LEFT = 3 ## - Return the world such that north is down the -X axis (rotate 90 degrees clockwise) -class RotatedRegionSet(RegionSet): +class RotatedRegionSet(RegionSetWrapper): """A regionset, only rotated such that north points in the given direction """ @@ -433,39 +457,41 @@ class RotatedRegionSet(RegionSet): _ROTATE_180 = lambda x,z: (-x,-z) # These take rotated coords and translate into un-rotated coords - _unrotation_funcs = { - 0: _NO_ROTATION, - 1: _ROTATE_COUNTERCLOCKWISE, - 2: _ROTATE_180, - 3: _ROTATE_CLOCKWISE, - } + _unrotation_funcs = [ + _NO_ROTATION, + _ROTATE_COUNTERCLOCKWISE, + _ROTATE_180, + _ROTATE_CLOCKWISE, + ] # These translate un-rotated coordinates into rotated coordinates - _rotation_funcs = { - 0: _NO_ROTATION, - 1: _ROTATE_CLOCKWISE, - 2: _ROTATE_180, - 3: _ROTATE_COUNTERCLOCKWISE, - } + _rotation_funcs = [ + _NO_ROTATION, + _ROTATE_CLOCKWISE, + _ROTATE_180, + _ROTATE_COUNTERCLOCKWISE, + ] - def __init__(self, regiondir, north_dir): + def __init__(self, rsetobj, north_dir): self.north_dir = north_dir self.unrotate = self._unrotation_funcs[north_dir] self.rotate = self._rotation_funcs[north_dir] - super(RotatedRegionSet, self).__init__(regiondir) + super(RotatedRegionSet, self).__init__(rsetobj) - # Re-initialize upon unpickling + # Re-initialize upon unpickling. This is needed because we store a couple + # lambda functions as instance variables def __getstate__(self): - return (self.regiondir, self.north_dir) + return (self._r, self.north_dir) def __setstate__(self, args): self.__init__(args[0], args[1]) def get_chunk(self, x, z): x,z = self.unrotate(x,z) - chunk_data = super(RotatedRegionSet, self).get_chunk(x,z) + chunk_data = dict(super(RotatedRegionSet, self).get_chunk(x,z)) for section in chunk_data['Sections']: + section = dict(section) for arrayname in ['Blocks', 'Data', 'SkyLight', 'BlockLight']: array = section[arrayname] # Since the anvil change, arrays are arranged with axes Y,Z,X