diff --git a/overviewer_core/src/iterate.c b/overviewer_core/src/iterate.c index 3e6e27a..5eceba9 100644 --- a/overviewer_core/src/iterate.c +++ b/overviewer_core/src/iterate.c @@ -146,9 +146,9 @@ int load_chunk(RenderState* state, int x, int z, unsigned char required) { if (sections) { sections = PySequence_Fast(sections, "Sections tag was not a list!"); } - Py_DECREF(chunk); if (sections == NULL) { // exception set, again + Py_DECREF(chunk); if (!required) { PyErr_Clear(); } @@ -156,6 +156,7 @@ int load_chunk(RenderState* state, int x, int z, unsigned char required) { } /* set up reasonable defaults */ + dest->biomes = NULL; for (i = 0; i < SECTIONS_PER_CHUNK; i++) { dest->sections[i].blocks = NULL; @@ -164,6 +165,9 @@ int load_chunk(RenderState* state, int x, int z, unsigned char required) { dest->sections[i].blocklight = NULL; } + dest->biomes = PyDict_GetItemString(chunk, "Biomes"); + Py_INCREF(dest->biomes); + for (i = 0; i < PySequence_Fast_GET_SIZE(sections); i++) { PyObject *ycoord = NULL; int sectiony = 0; @@ -177,6 +181,7 @@ int load_chunk(RenderState* state, int x, int z, unsigned char required) { load_chunk_section(dest, sectiony, section); } Py_DECREF(sections); + Py_DECREF(chunk); dest->loaded = 1; return 0; @@ -573,6 +578,7 @@ chunk_render(PyObject *self, PyObject *args) { for (j = 0; j < 3; j++) { if (state.chunks[i][j].loaded) { int k; + Py_XDECREF(state.chunks[i][j].biomes); for (k = 0; k < SECTIONS_PER_CHUNK; k++) { Py_XDECREF(state.chunks[i][j].sections[k].blocks); Py_XDECREF(state.chunks[i][j].sections[k].data); diff --git a/overviewer_core/src/overviewer.h b/overviewer_core/src/overviewer.h index 62e32f5..d402795 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 24 +#define OVERVIEWER_EXTENSION_VERSION 25 /* Python PIL, and numpy headers */ #include @@ -38,11 +38,13 @@ in y/z/x order */ #define getArrayByte3D(array, x,y,z) (*(unsigned char *)(PyArray_GETPTR3((array), (y), (z), (x)))) #define getArrayShort3D(array, x,y,z) (*(unsigned short *)(PyArray_GETPTR3((array), (y), (z), (x)))) +#define getArrayByte2D(array, x,y) (*(unsigned char *)(PyArray_GETPTR2((array), (x), (y)))) #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)) #define MIN(a, b) ((a) < (b) ? (a) : (b)) +#define CLAMP(x, a, b) (MIN(MAX(x, a), b)) /* in composite.c */ Imaging imaging_python_to_c(PyObject *obj); @@ -71,6 +73,8 @@ typedef struct _RenderMode RenderMode; typedef struct { /* whether this chunk is loaded: use load_chunk to load */ int loaded; + /* chunk biome array */ + PyObject *biomes; /* all the sections in a given chunk */ struct { /* all there is to know about each section */ @@ -143,6 +147,7 @@ typedef enum DATA, BLOCKLIGHT, SKYLIGHT, + BIOMES, } DataType; static inline unsigned int get_data(RenderState *state, DataType type, int x, int y, int z) { @@ -198,6 +203,8 @@ static inline unsigned int get_data(RenderState *state, DataType type, int x, in case SKYLIGHT: data_array = state->chunks[chunkx][chunkz].sections[chunky].skylight; break; + case BIOMES: + data_array = state->chunks[chunkx][chunkz].biomes; }; if (data_array == NULL) @@ -205,6 +212,8 @@ static inline unsigned int get_data(RenderState *state, DataType type, int x, in if (type == BLOCKS) return getArrayShort3D(data_array, x, y, z); + if (type == BIOMES) + return getArrayByte2D(data_array, x, z); return getArrayByte3D(data_array, x, y, z); } diff --git a/overviewer_core/src/primitives/base.c b/overviewer_core/src/primitives/base.c index 80622f0..5de8a1f 100644 --- a/overviewer_core/src/primitives/base.c +++ b/overviewer_core/src/primitives/base.c @@ -18,30 +18,68 @@ #include "../overviewer.h" typedef struct { - /* biome data for the chunk */ - PyObject *biome_data; /* grasscolor and foliagecolor lookup tables */ PyObject *grasscolor, *foliagecolor, *watercolor; /* biome-compatible grass/leaf textures */ PyObject *grass_texture; } PrimitiveBase; +typedef struct { + const char* name; + float temperature; + float rainfall; +} Biome; + +/* each entry in this table is yanked *directly* out of the minecraft source + * temp/rainfall are taken from what MCP calls setTemperatureRainfall + * + * keep in mind the x/y coordinate in the color tables is found *after* + * multiplying rainfall and temperature for the second coordinate, *and* the + * origin is in the lower-right. <3 biomes. + */ +static Biome biome_table[] = { + /* 0 */ + {"Ocean", 0.5, 0.5}, + {"Plains", 0.8, 0.4}, + {"Desert", 2.0, 0.0}, + {"Extreme Hills", 0.2, 0.3}, + {"Forest", 0.7, 0.8}, + /* 5 */ + {"Taiga", 0.05, 0.8}, + {"Swampland", 0.8, 0.9}, + {"River", 0.5, 0.5}, + {"Hell", 2.0, 0.0}, + {"Sky", 0.5, 0.5}, + /* 10 */ + {"FrozenOcean", 0.0, 0.5}, + {"FrozenRiver", 0.0, 0.5}, + {"Ice Plains", 0.0, 0.5}, + {"Ice Mountains", 0.0, 0.5}, + {"MushroomIsland", 0.9, 1.0}, + /* 15 */ + {"MushroomIslandShore", 0.9, 1.0}, + {"Beach", 0.8, 0.4}, + {"DesertHills", 2.0, 0.0}, + {"ForestHills", 0.7, 0.8}, + {"TaigaHills", 0.05, 0.8}, + /* 20 */ + {"Extreme Hills Edge", 0.2, 0.3}, + {"Jungle", 2.0, 0.45}, /* <-- GUESS, but a good one */ +}; + +#define NUM_BIOMES (sizeof(biome_table) / sizeof(Biome)) + static int base_start(void *data, RenderState *state, PyObject *support) { PrimitiveBase *self = (PrimitiveBase *)data; /* biome-compliant grass mask (includes sides!) */ self->grass_texture = PyObject_GetAttrString(state->textures, "biome_grass_texture"); - - 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", ""); - } + + /* color lookup tables */ + 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; } @@ -50,11 +88,10 @@ static void base_finish(void *data, RenderState *state) { PrimitiveBase *self = (PrimitiveBase *)data; - Py_XDECREF(self->biome_data); - Py_XDECREF(self->foliagecolor); - Py_XDECREF(self->grasscolor); - Py_XDECREF(self->watercolor); - Py_XDECREF(self->grass_texture); + Py_DECREF(self->foliagecolor); + Py_DECREF(self->grasscolor); + Py_DECREF(self->watercolor); + Py_DECREF(self->grass_texture); } static int @@ -72,13 +109,6 @@ base_occluded(void *data, RenderState *state, int x, int y, int z) { return 0; } -static int -base_hidden(void *data, RenderState *state, int x, int y, int z) { - PrimitiveBase *self = (PrimitiveBase *)data; - - return 0; -} - static void base_draw(void *data, RenderState *state, PyObject *src, PyObject *mask, PyObject *mask_light) { PrimitiveBase *self = (PrimitiveBase *)data; @@ -91,9 +121,8 @@ base_draw(void *data, RenderState *state, PyObject *src, PyObject *mask, PyObjec * NOTES for maintainers: * * To add a biome-compatible block, add an OR'd condition to this - * following if block, a case to the first switch statement to handle when - * biome info IS available, and another case to the second switch - * statement for when biome info ISN'T available. + * following if block, and a case to the switch statement to handle biome + * coloring. * * Make sure that in textures.py, the generated textures are the * biome-compliant ones! The tinting is now all done here. @@ -116,113 +145,102 @@ base_draw(void *data, RenderState *state, PyObject *src, PyObject *mask, PyObjec { /* do the biome stuff! */ PyObject *facemask = mask; - unsigned char r, g, b; + unsigned char r = 255, g = 255, b = 255; + PyObject *color_table = NULL; + unsigned char flip_xy = 0; if (state->block == 2) { /* grass needs a special facemask */ facemask = self->grass_texture; } - - if (self->biome_data) { - /* we have data, so use it! */ - unsigned int index; + + switch (state->block) { + case 2: + /* grass */ + color_table = self->grasscolor; + break; + case 8: + case 9: + /* water */ + color_table = self->watercolor; + break; + case 18: + /* leaves */ + color_table = self->foliagecolor; + if (state->block_data == 2) + { + /* birch! + birch foliage color is flipped XY-ways */ + flip_xy = 1; + } + break; + case 31: + /* tall grass */ + color_table = self->grasscolor; + break; + case 104: + /* pumpkin stem */ + color_table = self->grasscolor; + break; + case 105: + /* melon stem */ + color_table = self->grasscolor; + break; + case 106: + /* vines */ + color_table = self->grasscolor; + break; + case 111: + /* lily pads */ + color_table = self->grasscolor; + break; + default: + break; + }; + + if (color_table) { + int dx, dz; + unsigned char tablex, tabley; + float temp = 0.0, rain = 0.0; PyObject *color = NULL; - index = big_endian_ushort(getArrayShort2D(self->biome_data, state->x, state->y)); - - switch (state->block) { - case 2: - /* grass */ - color = PySequence_GetItem(self->grasscolor, index); - break; - case 8: - case 9: - /* water */ - if (self->watercolor) - { - color = PySequence_GetItem(self->watercolor, index); - } else { - color = NULL; - facemask = NULL; - } - break; - case 18: - /* leaves */ - if (state->block_data != 2) - { - /* not birch! */ - color = PySequence_GetItem(self->foliagecolor, index); - } else { - /* birch! - birch foliage color is flipped XY-ways */ - unsigned int index_x = 255 - (index % 256); - unsigned int index_y = 255 - (index / 256); - index = index_y * 256 + index_x; + /* average over all neighbors */ + for (dx = -1; dx <= 1; dx++) { + for (dz = -1; dz <= 1; dz += (dx == 0 ? 2 : 1)) { + unsigned char biome = get_data(state, BIOMES, state->x + dx, state->y, state->z + dz); + if (biome > NUM_BIOMES) + biome = 0; - color = PySequence_GetItem(self->foliagecolor, index); + temp += biome_table[biome].temperature; + rain += biome_table[biome].rainfall; } - break; - case 31: - /* tall grass */ - color = PySequence_GetItem(self->grasscolor, index); - break; - case 104: - /* pumpkin stem */ - color = PySequence_GetItem(self->grasscolor, index); - break; - case 105: - /* melon stem */ - color = PySequence_GetItem(self->grasscolor, index); - break; - case 106: - /* vines */ - color = PySequence_GetItem(self->grasscolor, index); - break; - case 111: - /* lily pads */ - color = PySequence_GetItem(self->grasscolor, index); - break; - default: - break; - }; - - if (color) - { - /* we've got work to do */ - - r = PyInt_AsLong(PyTuple_GET_ITEM(color, 0)); - g = PyInt_AsLong(PyTuple_GET_ITEM(color, 1)); - b = PyInt_AsLong(PyTuple_GET_ITEM(color, 2)); - Py_DECREF(color); } - } else { - if (state->block == 2 || state->block == 31 || - state->block == 104 || state->block == 105) - /* grass and pumpkin/melon stems */ - { - r = 115; - g = 175; - b = 71; + temp /= 8.0; + rain /= 8.0; + + /* make sure they're sane */ + temp = CLAMP(temp, 0.0, 1.0); + rain = CLAMP(rain, 0.0, 1.0); + + /* convert to x/y coordinates in color table */ + tablex = 255 - (255 * temp); + tabley = 255 - (255 * temp * rain); + if (flip_xy) { + unsigned char tmp = 255 - tablex; + tablex = 255 - tabley; + tabley = tmp; } - if (state->block == 8 || state->block == 9) - /* water */ - { - /* by default water is fine with nothing */ - facemask = NULL; - } - - if (state->block == 18 || state->block == 106 || state->block == 111) - /* leaves, vines and lyli pads */ - { - r = 37; - g = 118; - b = 25; - } + /* look up color! */ + color = PySequence_GetItem(color_table, tabley * 256 + tablex); + r = PyInt_AsLong(PyTuple_GET_ITEM(color, 0)); + g = PyInt_AsLong(PyTuple_GET_ITEM(color, 1)); + b = PyInt_AsLong(PyTuple_GET_ITEM(color, 2)); + Py_DECREF(color); } - if (facemask) - tint_with_mask(state->img, r, g, b, 255, facemask, state->imgx, state->imgy, 0, 0); + /* final coloration */ + tint_with_mask(state->img, r, g, b, 255, facemask, state->imgx, state->imgy, 0, 0); } } @@ -231,6 +249,6 @@ RenderPrimitiveInterface primitive_base = { base_start, base_finish, base_occluded, - base_hidden, + NULL, base_draw, }; diff --git a/overviewer_core/world.py b/overviewer_core/world.py index ff4dd6e..dd67180 100644 --- a/overviewer_core/world.py +++ b/overviewer_core/world.py @@ -32,14 +32,11 @@ This module has routines for extracting information about available worlds class ChunkDoesntExist(Exception): pass -class BiomeDataDoesntExist(Exception): - pass - def log_other_exceptions(func): """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. + ChunkDoesntExist 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) @@ -48,8 +45,6 @@ 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 @@ -239,7 +234,6 @@ class RegionSet(object): # Caching implementaiton: a simple LRU cache # 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 @@ -264,45 +258,6 @@ class RegionSet(object): 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): @@ -507,11 +462,6 @@ class RotatedRegionSet(RegionSet): 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)