diff --git a/overviewer_core/src/overviewer.h b/overviewer_core/src/overviewer.h index bef0316..78a0f46 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 18 +#define OVERVIEWER_EXTENSION_VERSION 19 /* Python PIL, and numpy headers */ #include @@ -35,7 +35,7 @@ /* macro for getting a value out of various numpy arrays */ #define getArrayByte3D(array, x,y,z) (*(unsigned char *)(PyArray_GETPTR3((array), (x), (y), (z)))) -#define getArrayShort1D(array, x) (*(unsigned short *)(PyArray_GETPTR1((array), (x)))) +#define getArrayShort2D(array, x,y) (*(unsigned short *)(PyArray_GETPTR2((array), (x), (y)))) /* generally useful MAX / MIN macros */ #define MAX(a, b) ((a) > (b) ? (a) : (b)) diff --git a/overviewer_core/src/primitives/base.c b/overviewer_core/src/primitives/base.c index 4f13c47..6c8d6b5 100644 --- a/overviewer_core/src/primitives/base.c +++ b/overviewer_core/src/primitives/base.c @@ -18,9 +18,7 @@ #include "../overviewer.h" typedef struct { - /* coordinates of the chunk, inside its region file */ - int chunk_x, chunk_y; - /* biome data for the region */ + /* biome data for the chunk */ PyObject *biome_data; /* grasscolor and foliagecolor lookup tables */ PyObject *grasscolor, *foliagecolor, *watercolor; @@ -35,44 +33,15 @@ base_start(void *data, RenderState *state, PyObject *support) { /* biome-compliant grass mask (includes sides!) */ self->grass_texture = PyObject_GetAttrString(state->textures, "biome_grass_texture"); - /* careful now -- C's % operator works differently from python's - we can't just do x % 32 like we did before */ - self->chunk_x = state->chunkx; - self->chunk_y = state->chunkz; - - while (self->chunk_x < 0) - self->chunk_x += 32; - while (self->chunk_y < 0) - self->chunk_y += 32; - - self->chunk_x %= 32; - self->chunk_y %= 32; - - /* XXX ignore biomes for now :( */ - /*if (PyObject_IsTrue(use_biomes)) { - self->biome_data = PyObject_CallMethod(state->textures, "getBiomeData", "OOO", - worlddir, chunk_x_py, chunk_y_py); - if (self->biome_data == Py_None) { - Py_DECREF(self->biome_data); - self->biome_data = NULL; - self->foliagecolor = NULL; - self->grasscolor = NULL; - } else { - self->foliagecolor = PyObject_GetAttrString(state->textures, "foliagecolor"); - self->grasscolor = PyObject_GetAttrString(state->textures, "grasscolor"); - self->watercolor = PyObject_GetAttrString(state->textures, "watercolor"); - if (self->watercolor == Py_None) - { - Py_DECREF(self->watercolor); - self->watercolor = NULL; - } - } - } else {*/ - self->biome_data = NULL; - self->foliagecolor = NULL; - self->grasscolor = NULL; - self->watercolor = NULL; - /*}*/ + self->biome_data = PyObject_CallMethod(state->regionset, "get_biome_data", "ii", state->chunkx, state->chunkz); + if (self->biome_data == NULL) { + /* error while loading biome info, or no biomes at all */ + PyErr_Clear(); + } else { + self->foliagecolor = PyObject_CallMethod(state->textures, "load_foliage_color", ""); + self->grasscolor = PyObject_CallMethod(state->textures, "load_grass_color", ""); + self->watercolor = PyObject_CallMethod(state->textures, "load_water_color", ""); + } return 0; } @@ -159,8 +128,7 @@ base_draw(void *data, RenderState *state, PyObject *src, PyObject *mask, PyObjec unsigned int index; PyObject *color = NULL; - index = ((self->chunk_y * 16) + state->y) * 16 * 32 + (self->chunk_x * 16) + state->x; - index = big_endian_ushort(getArrayShort1D(self->biome_data, index)); + index = big_endian_ushort(getArrayShort2D(self->biome_data, state->x, state->y)); switch (state->block) { case 2: @@ -211,7 +179,7 @@ base_draw(void *data, RenderState *state, PyObject *src, PyObject *mask, PyObjec color = PySequence_GetItem(self->grasscolor, index); break; case 111: - /* lily padas */ + /* lily pads */ color = PySequence_GetItem(self->grasscolor, index); break; default: diff --git a/overviewer_core/textures.py b/overviewer_core/textures.py index 88af019..8aeab61 100644 --- a/overviewer_core/textures.py +++ b/overviewer_core/textures.py @@ -56,7 +56,7 @@ class Textures(object): def __getstate__(self): # we must get rid of the huge image lists, and other images attributes = self.__dict__.copy() - for attr in ['terrain_images', 'blockmap', 'biome_grass_texture', 'watertexture', 'lavatexture', 'firetexture', 'portaltexture', 'lightcolor']: + for attr in ['terrain_images', 'blockmap', 'biome_grass_texture', 'watertexture', 'lavatexture', 'firetexture', 'portaltexture', 'lightcolor', 'grasscolor', 'foliagecolor', 'watercolor']: try: del attributes[attr] except KeyError: @@ -294,6 +294,24 @@ class Textures(object): lightcolor = None self.lightcolor = lightcolor return lightcolor + + def load_grass_color(self): + """Helper function to load the grass color texture.""" + if not hasattr(self, "grasscolor"): + self.grasscolor = list(self.load_image("grasscolor.png").getdata()) + return self.grasscolor + + def load_foliage_color(self): + """Helper function to load the foliage color texture.""" + if not hasattr(self, "foliagecolor"): + self.foliagecolor = list(self.load_image("foliagecolor.png").getdata()) + return self.foliagecolor + + def load_water_color(self): + """Helper function to load the water color texture.""" + if not hasattr(self, "watercolor"): + self.watercolor = list(self.load_image("watercolor.png").getdata()) + return self.watercolor def _split_terrain(self, terrain): """Builds and returns a length 256 array of each 16x16 chunk diff --git a/overviewer_core/world.py b/overviewer_core/world.py index b927fb9..ed09490 100644 --- a/overviewer_core/world.py +++ b/overviewer_core/world.py @@ -18,7 +18,6 @@ import os import os.path from glob import glob import logging -import collections import numpy @@ -30,35 +29,17 @@ This module has routines for extracting information about available worlds """ -base36decode = functools.partial(int, base=36) -cached = collections.defaultdict(dict) +class ChunkDoesntExist(Exception): + pass -def base36encode(number, alphabet='0123456789abcdefghijklmnopqrstuvwxyz'): - ''' - Convert an integer to a base36 string. - ''' - if not isinstance(number, (int, long)): - raise TypeError('number must be an integer') - - newn = abs(number) - - # Special case for zero - if number == 0: - return '0' - - base36 = '' - while newn != 0: - newn, i = divmod(newn, len(alphabet)) - base36 = alphabet[i] + base36 - - if number < 0: - return "-" + base36 - return base36 +class BiomeDataDoesntExist(Exception): + pass 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. + """A decorator that prints out any errors that are not + ChunkDoesntExist or BiomeDataDoesntExist errors. This decorates + get_chunk because the C code is likely to swallow exceptions, so + this will at least make them visible. """ functools.wraps(func) @@ -67,6 +48,8 @@ def log_other_exceptions(func): return func(*args) except ChunkDoesntExist: raise + except BiomeDataDoesntExist: + raise except Exception, e: logging.exception("%s raised this exception", func.func_name) raise @@ -150,10 +133,6 @@ class World(object): # TODO figure out where to handle regionlists - - self.useBiomeData = os.path.exists(os.path.join(worlddir, 'biomes')) - if not self.useBiomeData: - logging.info("Notice: Not using biome data for tinting") def get_regionsets(self): return self.regionsets @@ -259,7 +238,8 @@ class RegionSet(object): logging.debug("Done scanning regions") # Caching implementaiton: a simple LRU cache - # Decorate the get_chunk method with the cache decorator + # Decorate the getter methods with the cache decorator + self._get_biome_data_for_region = cache.lru_cache(cachesize)(self._get_biome_data_for_region) self.get_chunk = cache.lru_cache(cachesize)(self.get_chunk) # Re-initialize upon unpickling @@ -283,7 +263,47 @@ class RegionSet(object): return "overworld" else: raise Exception("Woah, what kind of dimension is this! %r" % self.regiondir) + + # this is decorated with cache.lru_cache in __init__(). Be aware! + @log_other_exceptions + def _get_biome_data_for_region(self, regionx, regionz): + """Get the block of biome data for an entire region. Biome + data is in the format output by Minecraft Biome Extractor: + http://code.google.com/p/minecraft-biome-extractor/""" + + # biomes only make sense for the overworld, right now + if self.get_type() != "overworld": + raise BiomeDataDoesntExist("Biome data is not available for '%s'." % (self.get_type(),)) + + # biomes are, unfortunately, in a different place than regiondir + biomefile = os.path.split(self.regiondir)[0] + biomefile = os.path.join(biomefile, 'biomes', 'b.%d.%d.biome' % (regionx, regionz)) + + try: + with open(biomefile, 'rb') as f: + data = f.read() + if not len(data) == 512 * 512 * 2: + raise BiomeDataDoesntExist("File `%s' does not have correct size." % (biomefile,)) + data = numpy.frombuffer(data, dtype=numpy.dtype(">u2")) + # reshape and transpose to get [x, z] indices + return numpy.transpose(numpy.reshape(data, (512, 512))) + except IOError: + raise BiomeDataDoesntExist("File `%s' could not be read." % (biomefile,)) + + @log_other_exceptions + def get_biome_data(self, x, z): + """Get the block of biome data for the given chunk. Biome data + is returned as a 16x16 numpy array of indices into the + corresponding biome color images.""" + regionx = x // 32 + regionz = z // 32 + blockx = (x % 32) * 16 + blockz = (z % 32) * 16 + + region_biomes = self._get_biome_data_for_region(regionx, regionz) + return region_biomes[blockx:blockx+16,blockz:blockz+16] + # this is decorated with cache.lru_cache in __init__(). Be aware! @log_other_exceptions def get_chunk(self, x, z): """Returns a dictionary object representing the "Level" NBT Compound @@ -448,7 +468,12 @@ class RotatedRegionSet(RegionSet): return (self.regiondir, self.north_dir) def __setstate__(self, args): self.__init__(args[0], args[1]) - + + def get_biome_data(self, x, z): + x,z = self.unrotate(x,z) + biome_data = super(RotatedRegionSet, self).get_biome_data(x,z) + return numpy.rot90(biome_data, self.north_dir) + def get_chunk(self, x, z): x,z = self.unrotate(x,z) chunk_data = super(RotatedRegionSet, self).get_chunk(x,z) @@ -467,9 +492,6 @@ class RotatedRegionSet(RegionSet): x,z = self.rotate(x,z) yield x,z,mtime -class ChunkDoesntExist(Exception): - pass - def get_save_dir(): """Returns the path to the local saves directory * On Windows, at %APPDATA%/.minecraft/saves/