# 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 sys import imp import os import os.path import zipfile from cStringIO import StringIO import math from random import randint import numpy from PIL import Image, ImageEnhance, ImageOps, ImageDraw import logging import functools import util import composite ## ## useful global variables ## # user-provided path given by --texture-path _find_file_local_path = None # image background color to use bgcolor = None # an array of the textures in terrain.png, split up terrain_images = None ## ## Helpers for opening textures ## def _find_file(filename, mode="rb", verbose=False): """Searches for the given file and returns an open handle to it. This searches the following locations in this order: * the textures_path given in the config file (if present) * The program dir (same dir as overviewer.py) * The overviewer_core/data/textures dir * On Darwin, in /Applications/Minecraft * Inside minecraft.jar, which is looked for at these locations * On Windows, at %APPDATA%/.minecraft/bin/minecraft.jar * On Darwin, at $HOME/Library/Application Support/minecraft/bin/minecraft.jar * at $HOME/.minecraft/bin/minecraft.jar """ if _find_file_local_path: path = os.path.join(_find_file_local_path, filename) if os.path.exists(path): if verbose: logging.info("Found %s in '%s'", filename, path) return open(path, mode) programdir = util.get_program_path() path = os.path.join(programdir, filename) if os.path.exists(path): if verbose: logging.info("Found %s in '%s'", filename, path) return open(path, mode) path = os.path.join(programdir, "overviewer_core", "data", "textures", filename) if os.path.exists(path): return open(path, mode) elif hasattr(sys, "frozen") or imp.is_frozen("__main__"): # windows special case, when the package dir doesn't exist path = os.path.join(programdir, "textures", filename) if os.path.exists(path): if verbose: logging.info("Found %s in '%s'", filename, path) return open(path, mode) if sys.platform == "darwin": path = os.path.join("/Applications/Minecraft", filename) if os.path.exists(path): if verbose: logging.info("Found %s in '%s'", filename, path) return open(path, mode) # Find minecraft.jar. jarpaths = [] if "APPDATA" in os.environ: jarpaths.append( os.path.join(os.environ['APPDATA'], ".minecraft", "bin", "minecraft.jar")) if "HOME" in os.environ: jarpaths.append(os.path.join(os.environ['HOME'], "Library", "Application Support", "minecraft","bin","minecraft.jar")) jarpaths.append(os.path.join(os.environ['HOME'], ".minecraft", "bin", "minecraft.jar")) jarpaths.append(os.path.join(programdir,"minecraft.jar")) jarpaths.append(os.path.join(os.getcwd(), "minecraft.jar")) if _find_file_local_path: jarpaths.append(os.path.join(_find_file_local_path, "minecraft.jar")) for jarpath in jarpaths: if os.path.exists(jarpath): jar = zipfile.ZipFile(jarpath) for jarfilename in [filename, 'misc/' + filename, 'environment/' + filename]: try: if verbose: logging.info("Found %s in '%s'", jarfilename, jarpath) return jar.open(jarfilename) except (KeyError, IOError), e: pass raise IOError("Could not find the file `{0}'. You can either place it in the same place as overviewer.py, use --textures-path, or install the Minecraft client.".format(filename)) def _load_image(filename): """Returns an image object""" fileobj = _find_file(filename) buffer = StringIO(fileobj.read()) return Image.open(buffer).convert("RGBA") def _split_terrain(terrain): """Builds and returns a length 256 array of each 16x16 chunk of texture""" textures = [] (terrain_width, terrain_height) = terrain.size texture_resolution = terrain_width / 16 for y in xrange(16): for x in xrange(16): left = x*texture_resolution upper = y*texture_resolution right = left+texture_resolution lower = upper+texture_resolution region = terrain.transform( (16, 16), Image.EXTENT, (left,upper,right,lower), Image.BICUBIC) textures.append(region) return textures ## ## Image Transformation Functions ## def transform_image_top(img): """Takes a PIL image and rotates it left 45 degrees and shrinks the y axis by a factor of 2. Returns the resulting image, which will be 24x12 pixels """ # Resize to 17x17, since the diagonal is approximately 24 pixels, a nice # even number that can be split in half twice img = img.resize((17, 17), Image.ANTIALIAS) # Build the Affine transformation matrix for this perspective transform = numpy.matrix(numpy.identity(3)) # Translate up and left, since rotations are about the origin transform *= numpy.matrix([[1,0,8.5],[0,1,8.5],[0,0,1]]) # Rotate 45 degrees ratio = math.cos(math.pi/4) #transform *= numpy.matrix("[0.707,-0.707,0;0.707,0.707,0;0,0,1]") transform *= numpy.matrix([[ratio,-ratio,0],[ratio,ratio,0],[0,0,1]]) # Translate back down and right transform *= numpy.matrix([[1,0,-12],[0,1,-12],[0,0,1]]) # scale the image down by a factor of 2 transform *= numpy.matrix("[1,0,0;0,2,0;0,0,1]") transform = numpy.array(transform)[:2,:].ravel().tolist() newimg = img.transform((24,12), Image.AFFINE, transform) return newimg def transform_image_side(img): """Takes an image and shears it for the left side of the cube (reflect for the right side)""" # Size of the cube side before shear img = img.resize((12,12), Image.ANTIALIAS) # Apply shear transform = numpy.matrix(numpy.identity(3)) transform *= numpy.matrix("[1,0,0;-0.5,1,0;0,0,1]") transform = numpy.array(transform)[:2,:].ravel().tolist() newimg = img.transform((12,18), Image.AFFINE, transform) return newimg def transform_image_slope(img): """Takes an image and shears it in the shape of a slope going up in the -y direction (reflect for +x direction). Used for minetracks""" # Take the same size as trasform_image_side img = img.resize((12,12), Image.ANTIALIAS) # Apply shear transform = numpy.matrix(numpy.identity(3)) transform *= numpy.matrix("[0.75,-0.5,3;0.25,0.5,-3;0,0,1]") transform = numpy.array(transform)[:2,:].ravel().tolist() newimg = img.transform((24,24), Image.AFFINE, transform) return newimg def transform_image_angle(img, angle): """Takes an image an shears it in arbitrary angle with the axis of rotation being vertical. WARNING! Don't use angle = pi/2 (or multiplies), it will return a blank image (or maybe garbage). NOTE: angle is in the image not in game, so for the left side of a block angle = 30 degree. """ # Take the same size as trasform_image_side img = img.resize((12,12), Image.ANTIALIAS) # some values cos_angle = math.cos(angle) sin_angle = math.sin(angle) # function_x and function_y are used to keep the result image in the # same position, and constant_x and constant_y are the coordinates # for the center for angle = 0. constant_x = 6. constant_y = 6. function_x = 6.*(1-cos_angle) function_y = -6*sin_angle big_term = ( (sin_angle * (function_x + constant_x)) - cos_angle* (function_y + constant_y))/cos_angle # The numpy array is not really used, but is helpful to # see the matrix used for the transformation. transform = numpy.array([[1./cos_angle, 0, -(function_x + constant_x)/cos_angle], [-sin_angle/(cos_angle), 1., big_term ], [0, 0, 1.]]) transform = tuple(transform[0]) + tuple(transform[1]) newimg = img.transform((24,24), Image.AFFINE, transform) return newimg def build_block(top, side): """From a top texture and a side texture, build a block image. top and side should be 16x16 image objects. Returns a 24x24 image """ img = Image.new("RGBA", (24,24), bgcolor) original_texture = top.copy() top = transform_image_top(top) if not side: composite.alpha_over(img, top, (0,0), top) return img side = transform_image_side(side) otherside = side.transpose(Image.FLIP_LEFT_RIGHT) # Darken the sides slightly. These methods also affect the alpha layer, # so save them first (we don't want to "darken" the alpha layer making # the block transparent) sidealpha = side.split()[3] side = ImageEnhance.Brightness(side).enhance(0.9) side.putalpha(sidealpha) othersidealpha = otherside.split()[3] otherside = ImageEnhance.Brightness(otherside).enhance(0.8) otherside.putalpha(othersidealpha) composite.alpha_over(img, top, (0,0), top) composite.alpha_over(img, side, (0,6), side) composite.alpha_over(img, otherside, (12,6), otherside) # Manually touch up 6 pixels that leave a gap because of how the # shearing works out. This makes the blocks perfectly tessellate-able for x,y in [(13,23), (17,21), (21,19)]: # Copy a pixel to x,y from x-1,y img.putpixel((x,y), img.getpixel((x-1,y))) for x,y in [(3,4), (7,2), (11,0)]: # Copy a pixel to x,y from x+1,y img.putpixel((x,y), img.getpixel((x+1,y))) return img def build_full_block(top, side1, side2, side3, side4, bottom=None): """From a top texture, a bottom texture and 4 different side textures, build a full block with four differnts faces. All images should be 16x16 image objects. Returns a 24x24 image. Can be used to render any block. side1 is in the -y face of the cube (top left, east) side2 is in the +x (top right, south) side3 is in the -x (bottom left, north) side4 is in the +y (bottom right, west) A non transparent block uses top, side 3 and side 4. If top is a tuple then first item is the top image and the second item is an increment (integer) from 0 to 16 (pixels in the original minecraft texture). This increment will be used to crop the side images and to paste the top image increment pixels lower, so if you use an increment of 8, it willll draw a half-block. NOTE: this method uses the bottom of the texture image (as done in minecraft with beds and cackes) """ increment = 0 if isinstance(top, tuple): increment = int(round((top[1] / 16.)*12.)) # range increment in the block height in pixels (half texture size) crop_height = increment top = top[0] if side1 != None: side1 = side1.copy() ImageDraw.Draw(side1).rectangle((0, 0,16,crop_height),outline=(0,0,0,0),fill=(0,0,0,0)) if side2 != None: side2 = side2.copy() ImageDraw.Draw(side2).rectangle((0, 0,16,crop_height),outline=(0,0,0,0),fill=(0,0,0,0)) if side3 != None: side3 = side3.copy() ImageDraw.Draw(side3).rectangle((0, 0,16,crop_height),outline=(0,0,0,0),fill=(0,0,0,0)) if side4 != None: side4 = side4.copy() ImageDraw.Draw(side4).rectangle((0, 0,16,crop_height),outline=(0,0,0,0),fill=(0,0,0,0)) img = Image.new("RGBA", (24,24), bgcolor) # first back sides if side1 != None : side1 = transform_image_side(side1) side1 = side1.transpose(Image.FLIP_LEFT_RIGHT) # Darken this side. sidealpha = side1.split()[3] side1 = ImageEnhance.Brightness(side1).enhance(0.9) side1.putalpha(sidealpha) composite.alpha_over(img, side1, (0,0), side1) if side2 != None : side2 = transform_image_side(side2) # Darken this side. sidealpha2 = side2.split()[3] side2 = ImageEnhance.Brightness(side2).enhance(0.8) side2.putalpha(sidealpha2) composite.alpha_over(img, side2, (12,0), side2) if bottom != None : bottom = transform_image_top(bottom) composite.alpha_over(img, bottom, (0,12), bottom) # front sides if side3 != None : side3 = transform_image_side(side3) # Darken this side sidealpha = side3.split()[3] side3 = ImageEnhance.Brightness(side3).enhance(0.9) side3.putalpha(sidealpha) composite.alpha_over(img, side3, (0,6), side3) if side4 != None : side4 = transform_image_side(side4) side4 = side4.transpose(Image.FLIP_LEFT_RIGHT) # Darken this side sidealpha = side4.split()[3] side4 = ImageEnhance.Brightness(side4).enhance(0.8) side4.putalpha(sidealpha) composite.alpha_over(img, side4, (12,6), side4) if top != None : top = transform_image_top(top) composite.alpha_over(img, top, (0, increment), top) return img def build_sprite(side): """From a side texture, create a sprite-like texture such as those used for spiderwebs or flowers.""" img = Image.new("RGBA", (24,24), bgcolor) side = transform_image_side(side) otherside = side.transpose(Image.FLIP_LEFT_RIGHT) composite.alpha_over(img, side, (6,3), side) composite.alpha_over(img, otherside, (6,3), otherside) return img def build_billboard(tex): """From a texture, create a billboard-like texture such as those used for tall grass or melon stems.""" img = Image.new("RGBA", (24,24), bgcolor) front = tex.resize((14, 11), Image.ANTIALIAS) composite.alpha_over(img, front, (5,9)) return img def generate_opaque_mask(img): """ Takes the alpha channel of the image and generates a mask (used for lighting the block) that deprecates values of alpha smallers than 50, and sets every other value to 255. """ alpha = img.split()[3] return alpha.point(lambda a: int(min(a, 25.5) * 10)) def tintTexture(im, c): # apparently converting to grayscale drops the alpha channel? i = ImageOps.colorize(ImageOps.grayscale(im), (0,0,0), c) i.putalpha(im.split()[3]); # copy the alpha band back in. assuming RGBA return i def generate_texture_tuple(img): """ This takes an image and returns the needed tuple for the blockmap dictionary.""" if img is None: return None return (img, generate_opaque_mask(img)) ## ## Biomes ## currentBiomeFile = None currentBiomeData = None grasscolor = None foliagecolor = None watercolor = None _north = None def prepareBiomeData(worlddir): global grasscolor, foliagecolor, watercolor # skip if the color files are already loaded if grasscolor and foliagecolor: return biomeDir = os.path.join(worlddir, "biomes") if not os.path.exists(biomeDir): raise Exception("biomes not found") # try to find the biome color images. If _find_file can't locate them # then try looking in the EXTRACTEDBIOMES folder try: grasscolor = list(_load_image("grasscolor.png").getdata()) foliagecolor = list(_load_image("foliagecolor.png").getdata()) # don't force the water color just yet # since the biome extractor doesn't know about it try: watercolor = list(_load_image("watercolor.png").getdata()) except IOError: pass except IOError: try: grasscolor = list(Image.open(os.path.join(biomeDir,"grasscolor.png")).getdata()) foliagecolor = list(Image.open(os.path.join(biomeDir,"foliagecolor.png")).getdata()) except Exception: # clear anything that managed to get set grasscolor = None foliagecolor = None watercolor = None def getBiomeData(worlddir, chunkX, chunkY): '''Opens the worlddir and reads in the biome color information from the .biome files. See also: http://www.minecraftforum.net/viewtopic.php?f=25&t=80902 ''' global currentBiomeFile, currentBiomeData global _north biomeX = chunkX // 32 biomeY = chunkY // 32 rots = 0 if _north == 'upper-left': temp = biomeX biomeX = biomeY biomeY = -temp-1 rots = 3 elif _north == 'upper-right': biomeX = -biomeX-1 biomeY = -biomeY-1 rots = 2 elif _north == 'lower-right': temp = biomeX biomeX = -biomeY-1 biomeY = temp rots = 1 biomeFile = "b.%d.%d.biome" % (biomeX, biomeY) if biomeFile == currentBiomeFile: return currentBiomeData try: with open(os.path.join(worlddir, "biomes", biomeFile), "rb") as f: rawdata = f.read() # make sure the file size is correct if not len(rawdata) == 512 * 512 * 2: raise Exception("Biome file %s is not valid." % (biomeFile,)) data = numpy.reshape(numpy.rot90(numpy.reshape( numpy.frombuffer(rawdata, dtype=numpy.dtype(">u2")), (512,512)),rots), -1) except IOError: data = None pass # no biome data currentBiomeFile = biomeFile currentBiomeData = data return data ## ## Color Light ## lightcolor = None lightcolor_checked = False def loadLightColor(): global lightcolor, lightcolor_checked if not lightcolor_checked: lightcolor_checked = True try: lightcolor = list(_load_image("light_normal.png").getdata()) except Exception: logging.warning("Light color image could not be found.") lightcolor = None return lightcolor ## ## The big one: generate() and associated framework ## # placeholders that are generated in generate() texture_dimensions = None blockmap_generators = {} blockmap = {} biome_grass_texture = None transparent_blocks = set([0,]) solid_blocks = set() fluid_blocks = set() nospawn_blocks = set() # the material registration decorator def material(blockid=[], data=[0], **kwargs): # mapping from property name to the set to store them in properties = {"transparent" : transparent_blocks, "solid" : solid_blocks, "fluid" : fluid_blocks, "nospawn" : nospawn_blocks} # make sure blockid and data are iterable try: iter(blockid) except: blockid = [blockid,] try: iter(data) except: data = [data,] def inner_material(func): global blockmap_generators # create a wrapper function with a known signature @functools.wraps(func) def func_wrapper(blockid, data, north): try: return func(blockid, data, north) except TypeError: return func(blockid, data) for block in blockid: # set the property sets appropriately for prop in properties: try: if block in kwargs.get(prop, []): properties[prop].update([block]) except TypeError: if kwargs.get(prop, False): properties[prop].update([block]) # populate blockmap_generators with our function for d in data: blockmap_generators[(block, d)] = func_wrapper return func_wrapper return inner_material # shortcut function for pure blocks, default to solid def block(blockid=[], top_index=None, side_index=None, **kwargs): new_kwargs = {'solid' : True} new_kwargs.update(kwargs) if top_index is None: raise ValueError("top_index was not provided") if side_index is None: side_index = top_index @material(blockid=blockid, **new_kwargs) def inner_block(unused_id, unused_data): return build_block(terrain_images[top_index], terrain_images[side_index]) return inner_block # shortcut function for sprite blocks, defaults to transparent def sprite(blockid=[], index=None, **kwargs): new_kwargs = {'transparent' : True} new_kwargs.update(kwargs) if index is None: raise ValueError("index was not provided") @material(blockid=blockid, **new_kwargs) def inner_sprite(unused_id, unused_data): return build_sprite(terrain_images[index]) return inner_sprite # shortcut function for billboard blocks, defaults to transparent def billboard(blockid=[], index=None, **kwargs): new_kwargs = {'transparent' : True} new_kwargs.update(kwargs) if index is None: raise ValueError("index was not provided") @material(blockid=blockid, **new_kwargs) def inner_billboard(unused_id, unused_data): return build_billboard(terrain_images[index]) return inner_billboard def generate(path=None,texture_size=24,bgc = (26,26,26,0),north_direction='lower-left'): global _find_file_local_path global bgcolor global texture_dimensions global _north bgcolor = bgc _find_file_local_path = path _north = north_direction texture_dimensions = (texture_size, texture_size) # This maps terainids to 16x16 images global terrain_images terrain_images = _split_terrain(_load_image("terrain.png")) # generate biome grass mask global biome_grass_texture biome_grass_texture = build_block(terrain_images[0], terrain_images[38]) # generate the blocks global blockmap, blockmap_generators blockmap = {} for blockid, data in blockmap_generators: texgen = blockmap_generators[(blockid, data)] tex = texgen(blockid, data, north_direction) blockmap[(blockid, data)] = generate_texture_tuple(tex) if texture_size != 24: # rescale biome textures. biome_grass_texture = biome_grass_texture.resize(texture_dimensions, Image.ANTIALIAS) # rescale the special block images for blockid, data in iter(blockmap): block = blockmap[(blockid,data)] if block != None: block = block[0] scaled_block = block.resize(texture_dimensions, Image.ANTIALIAS) blockmap[(blockid,data)] = generate_texture_tuple(scaled_block, blockid) ## ## and finally: actual texture definitions ## # stone block(blockid=1, top_index=1) @material(blockid=2, data=range(11)+[0x10,], solid=True) def grass(blockid, data): # 0x10 bit means SNOW side_img = terrain_images[3] if data & 0x10: side_img = terrain_images[68] img = build_block(terrain_images[0], side_img) if not data & 0x10: global biome_grass_texture composite.alpha_over(img, biome_grass_texture, (0, 0), biome_grass_texture) return img # dirt block(blockid=3, top_index=2) # cobblestone block(blockid=4, top_index=16) # wooden plank block(blockid=5, top_index=4) @material(blockid=6, data=range(16), transparent=True) def saplings(blockid, data): # usual saplings tex = terrain_images[15] if data & 0x3 == 1: # spruce sapling tex = terrain_images[63] if data & 0x3 == 2: # birch sapling tex = terrain_images[79] return build_sprite(tex) # bedrock block(blockid=7, top_index=17) @material(blockid=8, data=range(16), fluid=True, transparent=True) def water(blockid, data): watertex = _load_image("water.png") return build_block(watertex, watertex) # other water, glass, and ice (no inner surfaces) # uses pseudo-ancildata found in iterate.c @material(blockid=[9, 20, 79], data=range(32), fluid=(9,), transparent=True, nospawn=True) def no_inner_surfaces(blockid, data): if blockid == 9: texture = _load_image("water.png") elif blockid == 20: texture = terrain_images[49] else: texture = terrain_images[67] if (data & 0b10000) == 16: top = texture else: top = None if (data & 0b0001) == 1: side1 = texture # top left else: side1 = None if (data & 0b1000) == 8: side2 = texture # top right else: side2 = None if (data & 0b0010) == 2: side3 = texture # bottom left else: side3 = None if (data & 0b0100) == 4: side4 = texture # bottom right else: side4 = None # if nothing shown do not draw at all if top is None and side3 is None and side4 is None: return None img = build_full_block(top,None,None,side3,side4) return img @material(blockid=[10, 11], data=range(16), fluid=True, transparent=False) def lava(blockid, data): lavatex = _load_image("lava.png") return build_block(lavatex, lavatex) # sand block(blockid=12, top_index=18) # gravel block(blockid=13, top_index=19) # gold ore block(blockid=14, top_index=32) # iron ore block(blockid=15, top_index=33) # coal ore block(blockid=16, top_index=34) @material(blockid=17, data=range(3), solid=True) def wood(blockid, data): top = terrain_images[21] if data == 0: # normal return build_block(top, terrain_images[20]) if data == 1: # birch return build_block(top, terrain_images[116]) if data == 2: # pine return build_block(top, terrain_images[117]) @material(blockid=18, data=range(16), transparent=True, solid=True) def leaves(blockid, data): t = terrain_images[52] if data == 1: # pine! t = terrain_images[132] return build_block(t, t) # sponge block(blockid=19, top_index=48) # lapis lazuli ore block(blockid=21, top_index=160) # lapis lazuli block block(blockid=22, top_index=144) # dispensers, furnaces, and burning furnaces @material(blockid=[23, 61, 62], data=range(6), solid=True) def furnaces(blockid, data, north): # first, do the north rotation if needed if north == 'upper-left': if data == 2: data = 5 elif data == 3: data = 4 elif data == 4: data = 2 elif data == 5: data = 3 elif north == 'upper-right': if data == 2: data = 3 elif data == 3: data = 2 elif data == 4: data = 5 elif data == 5: data = 4 elif north == 'lower-right': if data == 2: data = 4 elif data == 3: data = 5 elif data == 4: data = 3 elif data == 5: data = 2 top = terrain_images[62] side = terrain_images[45] if blockid == 61: front = terrain_images[44] elif blockid == 62: front = terrain_images[61] elif blockid == 23: front = terrain_images[46] if data == 3: # pointing west return build_full_block(top, None, None, side, front) elif data == 4: # pointing north return build_full_block(top, None, None, front, side) else: # in any other direction the front can't be seen return build_full_block(top, None, None, side, side) # sandstone block(blockid=24, top_index=176, side_index=192) # note block block(blockid=25, top_index=74) @material(blockid=26, data=range(12), transparent=True) def bed(blockid, data, north): # first get north rotation done # Masked to not clobber block head/foot info if north == 'upper-left': if (data & 0b0011) == 0: data = data & 0b1100 | 1 elif (data & 0b0011) == 1: data = data & 0b1100 | 2 elif (data & 0b0011) == 2: data = data & 0b1100 | 3 elif (data & 0b0011) == 3: data = data & 0b1100 | 0 elif north == 'upper-right': if (data & 0b0011) == 0: data = data & 0b1100 | 2 elif (data & 0b0011) == 1: data = data & 0b1100 | 3 elif (data & 0b0011) == 2: data = data & 0b1100 | 0 elif (data & 0b0011) == 3: data = data & 0b1100 | 1 elif north == 'lower-right': if (data & 0b0011) == 0: data = data & 0b1100 | 3 elif (data & 0b0011) == 1: data = data & 0b1100 | 0 elif (data & 0b0011) == 2: data = data & 0b1100 | 1 elif (data & 0b0011) == 3: data = data & 0b1100 | 2 increment = 8 left_face = None right_face = None if data & 0x8 == 0x8: # head of the bed top = terrain_images[135] if data & 0x00 == 0x00: # head pointing to West top = top.copy().rotate(270) left_face = terrain_images[151] right_face = terrain_images[152] if data & 0x01 == 0x01: # ... North top = top.rotate(270) left_face = terrain_images[152] right_face = terrain_images[151] if data & 0x02 == 0x02: # East top = top.rotate(180) left_face = terrain_images[151].transpose(Image.FLIP_LEFT_RIGHT) right_face = None if data & 0x03 == 0x03: # South right_face = None right_face = terrain_images[151].transpose(Image.FLIP_LEFT_RIGHT) else: # foot of the bed top = terrain_images[134] if data & 0x00 == 0x00: # head pointing to West top = top.rotate(270) left_face = terrain_images[150] right_face = None if data & 0x01 == 0x01: # ... North top = top.rotate(270) left_face = None right_face = terrain_images[150] if data & 0x02 == 0x02: # East top = top.rotate(180) left_face = terrain_images[150].transpose(Image.FLIP_LEFT_RIGHT) right_face = terrain_images[149].transpose(Image.FLIP_LEFT_RIGHT) if data & 0x03 == 0x03: # South left_face = terrain_images[149] right_face = terrain_images[150].transpose(Image.FLIP_LEFT_RIGHT) top = (top, increment) return build_full_block(top, None, None, left_face, right_face) # powered, detector, and normal rails @material(blockid=[27, 28, 66], data=range(14), transparent=True) def rails(blockid, data, north): # first, do north rotation # Masked to not clobber powered rail on/off info # Ascending and flat straight if north == 'upper-left': if (data & 0b0111) == 0: data = data & 0b1000 | 1 elif (data & 0b0111) == 1: data = data & 0b1000 | 0 elif (data & 0b0111) == 2: data = data & 0b1000 | 5 elif (data & 0b0111) == 3: data = data & 0b1000 | 4 elif (data & 0b0111) == 4: data = data & 0b1000 | 2 elif (data & 0b0111) == 5: data = data & 0b1000 | 3 elif north == 'upper-right': if (data & 0b0111) == 2: data = data & 0b1000 | 3 elif (data & 0b0111) == 3: data = data & 0b1000 | 2 elif (data & 0b0111) == 4: data = data & 0b1000 | 5 elif (data & 0b0111) == 5: data = data & 0b1000 | 4 elif north == 'lower-right': if (data & 0b0111) == 0: data = data & 0b1000 | 1 elif (data & 0b0111) == 1: data = data & 0b1000 | 0 elif (data & 0b0111) == 2: data = data & 0b1000 | 4 elif (data & 0b0111) == 3: data = data & 0b1000 | 5 elif (data & 0b0111) == 4: data = data & 0b1000 | 3 elif (data & 0b0111) == 5: data = data & 0b1000 | 2 img = Image.new("RGBA", (24,24), bgcolor) if blockid == 27: # powered rail if data & 0x8 == 0: # unpowered raw_straight = terrain_images[163] raw_corner = terrain_images[112] # they don't exist but make the code # much simplier elif data & 0x8 == 0x8: # powered raw_straight = terrain_images[179] raw_corner = terrain_images[112] # leave corners for code simplicity # filter the 'powered' bit data = data & 0x7 elif blockid == 28: # detector rail raw_straight = terrain_images[195] raw_corner = terrain_images[112] # leave corners for code simplicity elif blockid == 66: # normal rail raw_straight = terrain_images[128] raw_corner = terrain_images[112] ## use transform_image to scale and shear if data == 0: track = transform_image_top(raw_straight) composite.alpha_over(img, track, (0,12), track) elif data == 6: track = transform_image_top(raw_corner) composite.alpha_over(img, track, (0,12), track) elif data == 7: track = transform_image_top(raw_corner.rotate(270)) composite.alpha_over(img, track, (0,12), track) elif data == 8: # flip track = transform_image_top(raw_corner.transpose(Image.FLIP_TOP_BOTTOM).rotate(90)) composite.alpha_over(img, track, (0,12), track) elif data == 9: track = transform_image_top(raw_corner.transpose(Image.FLIP_TOP_BOTTOM)) composite.alpha_over(img, track, (0,12), track) elif data == 1: track = transform_image_top(raw_straight.rotate(90)) composite.alpha_over(img, track, (0,12), track) #slopes elif data == 2: # slope going up in +x direction track = transform_image_slope(raw_straight) track = track.transpose(Image.FLIP_LEFT_RIGHT) composite.alpha_over(img, track, (2,0), track) # the 2 pixels move is needed to fit with the adjacent tracks elif data == 3: # slope going up in -x direction # tracks are sprites, in this case we are seeing the "side" of # the sprite, so draw a line to make it beautiful. ImageDraw.Draw(img).line([(11,11),(23,17)],fill=(164,164,164)) # grey from track texture (exterior grey). # the track doesn't start from image corners, be carefull drawing the line! elif data == 4: # slope going up in -y direction track = transform_image_slope(raw_straight) composite.alpha_over(img, track, (0,0), track) elif data == 5: # slope going up in +y direction # same as "data == 3" ImageDraw.Draw(img).line([(1,17),(12,11)],fill=(164,164,164)) return img # sticky and normal piston body @material(blockid=[29, 33], data=[0,1,2,3,4,5,8,9,10,11,12,13], transparent=True, solid=True) def piston(blockid, data, north): # first, north rotation # Masked to not clobber block head/foot info if north == 'upper-left': if (data & 0b0111) == 2: data = data & 0b1000 | 5 elif (data & 0b0111) == 3: data = data & 0b1000 | 4 elif (data & 0b0111) == 4: data = data & 0b1000 | 2 elif (data & 0b0111) == 5: data = data & 0b1000 | 3 elif north == 'upper-right': if (data & 0b0111) == 2: data = data & 0b1000 | 3 elif (data & 0b0111) == 3: data = data & 0b1000 | 2 elif (data & 0b0111) == 4: data = data & 0b1000 | 5 elif (data & 0b0111) == 5: data = data & 0b1000 | 4 elif north == 'lower-right': if (data & 0b0111) == 2: data = data & 0b1000 | 4 elif (data & 0b0111) == 3: data = data & 0b1000 | 5 elif (data & 0b0111) == 4: data = data & 0b1000 | 3 elif (data & 0b0111) == 5: data = data & 0b1000 | 2 if blockid == 29: # sticky piston_t = terrain_images[106].copy() else: # normal piston_t = terrain_images[107].copy() # other textures side_t = terrain_images[108].copy() back_t = terrain_images[109].copy() interior_t = terrain_images[110].copy() if data & 0x08 == 0x08: # pushed out, non full blocks, tricky stuff # remove piston texture from piston body ImageDraw.Draw(side_t).rectangle((0, 0,16,3),outline=(0,0,0,0),fill=(0,0,0,0)) if data & 0x07 == 0x0: # down side_t = side_t.rotate(180) img = build_full_block(back_t ,None ,None ,side_t, side_t) elif data & 0x07 == 0x1: # up img = build_full_block((interior_t, 4) ,None ,None ,side_t, side_t) elif data & 0x07 == 0x2: # east img = build_full_block(side_t , None, None ,side_t.rotate(90), back_t) elif data & 0x07 == 0x3: # west img = build_full_block(side_t.rotate(180) ,None ,None ,side_t.rotate(270), None) temp = transform_image_side(interior_t) temp = temp.transpose(Image.FLIP_LEFT_RIGHT) composite.alpha_over(img, temp, (9,5), temp) elif data & 0x07 == 0x4: # north img = build_full_block(side_t.rotate(90) ,None ,None , None, side_t.rotate(270)) temp = transform_image_side(interior_t) composite.alpha_over(img, temp, (3,5), temp) elif data & 0x07 == 0x5: # south img = build_full_block(side_t.rotate(270) ,None , None ,back_t, side_t.rotate(90)) else: # pushed in, normal full blocks, easy stuff if data & 0x07 == 0x0: # down side_t = side_t.rotate(180) img = build_full_block(back_t ,None ,None ,side_t, side_t) elif data & 0x07 == 0x1: # up img = build_full_block(piston_t ,None ,None ,side_t, side_t) elif data & 0x07 == 0x2: # east img = build_full_block(side_t ,None ,None ,side_t.rotate(90), back_t) elif data & 0x07 == 0x3: # west img = build_full_block(side_t.rotate(180) ,None ,None ,side_t.rotate(270), piston_t) elif data & 0x07 == 0x4: # north img = build_full_block(side_t.rotate(90) ,None ,None ,piston_t, side_t.rotate(270)) elif data & 0x07 == 0x5: # south img = build_full_block(side_t.rotate(270) ,None ,None ,back_t, side_t.rotate(90)) return img # sticky and normal piston shaft @material(blockid=34, data=[0,1,2,3,4,5,8,9,10,11,12,13], transparent=True) def piston_extension(blockid, data, north): # first, north rotation # Masked to not clobber block head/foot info if north == 'upper-left': if (data & 0b0111) == 2: data = data & 0b1000 | 5 elif (data & 0b0111) == 3: data = data & 0b1000 | 4 elif (data & 0b0111) == 4: data = data & 0b1000 | 2 elif (data & 0b0111) == 5: data = data & 0b1000 | 3 elif north == 'upper-right': if (data & 0b0111) == 2: data = data & 0b1000 | 3 elif (data & 0b0111) == 3: data = data & 0b1000 | 2 elif (data & 0b0111) == 4: data = data & 0b1000 | 5 elif (data & 0b0111) == 5: data = data & 0b1000 | 4 elif north == 'lower-right': if (data & 0b0111) == 2: data = data & 0b1000 | 4 elif (data & 0b0111) == 3: data = data & 0b1000 | 5 elif (data & 0b0111) == 4: data = data & 0b1000 | 3 elif (data & 0b0111) == 5: data = data & 0b1000 | 2 if (data & 0x8) == 0x8: # sticky piston_t = terrain_images[106].copy() else: # normal piston_t = terrain_images[107].copy() # other textures side_t = terrain_images[108].copy() back_t = terrain_images[107].copy() # crop piston body ImageDraw.Draw(side_t).rectangle((0, 4,16,16),outline=(0,0,0,0),fill=(0,0,0,0)) # generate the horizontal piston extension stick h_stick = Image.new("RGBA", (24,24), bgcolor) temp = transform_image_side(side_t) composite.alpha_over(h_stick, temp, (1,7), temp) temp = transform_image_top(side_t.rotate(90)) composite.alpha_over(h_stick, temp, (1,1), temp) # Darken it sidealpha = h_stick.split()[3] h_stick = ImageEnhance.Brightness(h_stick).enhance(0.85) h_stick.putalpha(sidealpha) # generate the vertical piston extension stick v_stick = Image.new("RGBA", (24,24), bgcolor) temp = transform_image_side(side_t.rotate(90)) composite.alpha_over(v_stick, temp, (12,6), temp) temp = temp.transpose(Image.FLIP_LEFT_RIGHT) composite.alpha_over(v_stick, temp, (1,6), temp) # Darken it sidealpha = v_stick.split()[3] v_stick = ImageEnhance.Brightness(v_stick).enhance(0.85) v_stick.putalpha(sidealpha) # Piston orientation is stored in the 3 first bits if data & 0x07 == 0x0: # down side_t = side_t.rotate(180) img = build_full_block((back_t, 12) ,None ,None ,side_t, side_t) composite.alpha_over(img, v_stick, (0,-3), v_stick) elif data & 0x07 == 0x1: # up img = Image.new("RGBA", (24,24), bgcolor) img2 = build_full_block(piston_t ,None ,None ,side_t, side_t) composite.alpha_over(img, v_stick, (0,4), v_stick) composite.alpha_over(img, img2, (0,0), img2) elif data & 0x07 == 0x2: # east img = build_full_block(side_t ,None ,None ,side_t.rotate(90), None) temp = transform_image_side(back_t).transpose(Image.FLIP_LEFT_RIGHT) composite.alpha_over(img, temp, (2,2), temp) composite.alpha_over(img, h_stick, (6,3), h_stick) elif data & 0x07 == 0x3: # west img = Image.new("RGBA", (24,24), bgcolor) img2 = build_full_block(side_t.rotate(180) ,None ,None ,side_t.rotate(270), piston_t) composite.alpha_over(img, h_stick, (0,0), h_stick) composite.alpha_over(img, img2, (0,0), img2) elif data & 0x07 == 0x4: # north img = build_full_block(side_t.rotate(90) ,None ,None , piston_t, side_t.rotate(270)) composite.alpha_over(img, h_stick.transpose(Image.FLIP_LEFT_RIGHT), (0,0), h_stick.transpose(Image.FLIP_LEFT_RIGHT)) elif data & 0x07 == 0x5: # south img = Image.new("RGBA", (24,24), bgcolor) img2 = build_full_block(side_t.rotate(270) ,None ,None ,None, side_t.rotate(90)) temp = transform_image_side(back_t) composite.alpha_over(img2, temp, (10,2), temp) composite.alpha_over(img, img2, (0,0), img2) composite.alpha_over(img, h_stick.transpose(Image.FLIP_LEFT_RIGHT), (-3,2), h_stick.transpose(Image.FLIP_LEFT_RIGHT)) return img # cobweb sprite(blockid=30, index=11) @material(blockid=31, data=range(3), transparent=True) def tall_grass(blockid, data): if data == 0: # dead shrub texture = terrain_images[55] elif data == 1: # tall grass texture = terrain_images[39] elif data == 2: # fern texture = terrain_images[56] return build_billboard(texture) # dead bush billboard(blockid=32, index=55) @material(blockid=35, data=range(16), solid=True) def wool(blockid, data): if data == 0: # white texture = terrain_images[64] elif data == 1: # orange texture = terrain_images[210] elif data == 2: # magenta texture = terrain_images[194] elif data == 3: # light blue texture = terrain_images[178] elif data == 4: # yellow texture = terrain_images[162] elif data == 5: # light green texture = terrain_images[146] elif data == 6: # pink texture = terrain_images[130] elif data == 7: # grey texture = terrain_images[114] elif data == 8: # light grey texture = terrain_images[225] elif data == 9: # cyan texture = terrain_images[209] elif data == 10: # purple texture = terrain_images[193] elif data == 11: # blue texture = terrain_images[177] elif data == 12: # brown texture = terrain_images[161] elif data == 13: # dark green texture = terrain_images[145] elif data == 14: # red texture = terrain_images[129] elif data == 15: # black texture = terrain_images[113] return build_block(texture, texture) # dandelion sprite(blockid=37, index=13) # rose sprite(blockid=38, index=12) # brown mushroom sprite(blockid=39, index=29) # red mushroom sprite(blockid=40, index=28) # block of gold block(blockid=41, top_index=23) # block of iron block(blockid=42, top_index=22) # double slabs and slabs @material(blockid=[43, 44], data=range(6), transparent=(44,), solid=(43,)) def slabs(blockid, data): if data == 0: # stone slab top = terrain_images[6] side = terrain_images[5] elif data == 1: # stone slab top = terrain_images[176] side = terrain_images[192] elif data == 2: # wooden slab top = side = terrain_images[4] elif data == 3: # cobblestone slab top = side = terrain_images[16] elif data == 4: # brick? top = side = terrain_images[7] elif data == 5: # stone brick? top = side = terrain_images[54] if blockid == 43: # double slab return build_block(top, side) # plain slab top = transform_image_top(top) side = transform_image_side(side) otherside = side.transpose(Image.FLIP_LEFT_RIGHT) sidealpha = side.split()[3] side = ImageEnhance.Brightness(side).enhance(0.9) side.putalpha(sidealpha) othersidealpha = otherside.split()[3] otherside = ImageEnhance.Brightness(otherside).enhance(0.8) otherside.putalpha(othersidealpha) img = Image.new("RGBA", (24,24), bgcolor) composite.alpha_over(img, side, (0,12), side) composite.alpha_over(img, otherside, (12,12), otherside) composite.alpha_over(img, top, (0,6), top) return img # brick block block(blockid=45, top_index=7) # TNT block(blockid=46, top_index=9, side_index=8) # bookshelf block(blockid=47, top_index=4, side_index=35) # moss stone block(blockid=48, top_index=36) # obsidian block(blockid=49, top_index=37) # torch, redstone torch (off), redstone torch(on) @material(blockid=[50, 75, 76], data=[1, 2, 3, 4, 5], transparent=True) def torches(blockid, data, north): # first, north rotations if north == 'upper-left': if data == 1: data = 3 elif data == 2: data = 4 elif data == 3: data = 2 elif data == 4: data = 1 elif north == 'upper-right': if data == 1: data = 2 elif data == 2: data = 1 elif data == 3: data = 4 elif data == 4: data = 3 elif north == 'lower-right': if data == 1: data = 4 elif data == 2: data = 3 elif data == 3: data = 1 elif data == 4: data = 2 # choose the proper texture if blockid == 50: # torch small = terrain_images[80] elif blockid == 75: # off redstone torch small = terrain_images[115] else: # on redstone torch small = terrain_images[99] # compose a torch bigger than the normal # (better for doing transformations) torch = Image.new("RGBA", (16,16), bgcolor) composite.alpha_over(torch,small,(-4,-3)) composite.alpha_over(torch,small,(-5,-2)) composite.alpha_over(torch,small,(-3,-2)) # angle of inclination of the texture rotation = 15 if data == 1: # pointing south torch = torch.rotate(-rotation, Image.NEAREST) # nearest filter is more nitid. img = build_full_block(None, None, None, torch, None, None) elif data == 2: # pointing north torch = torch.rotate(rotation, Image.NEAREST) img = build_full_block(None, None, torch, None, None, None) elif data == 3: # pointing west torch = torch.rotate(rotation, Image.NEAREST) img = build_full_block(None, torch, None, None, None, None) elif data == 4: # pointing east torch = torch.rotate(-rotation, Image.NEAREST) img = build_full_block(None, None, None, None, torch, None) elif data == 5: # standing on the floor # compose a "3d torch". img = Image.new("RGBA", (24,24), bgcolor) small_crop = small.crop((2,2,14,14)) slice = small_crop.copy() ImageDraw.Draw(slice).rectangle((6,0,12,12),outline=(0,0,0,0),fill=(0,0,0,0)) ImageDraw.Draw(slice).rectangle((0,0,4,12),outline=(0,0,0,0),fill=(0,0,0,0)) composite.alpha_over(img, slice, (7,5)) composite.alpha_over(img, small_crop, (6,6)) composite.alpha_over(img, small_crop, (7,6)) composite.alpha_over(img, slice, (7,7)) return img