diff --git a/chunk.py b/chunk.py index 8e6245e..01db090 100644 --- a/chunk.py +++ b/chunk.py @@ -128,54 +128,21 @@ fluid_blocks = set([8,9,10,11]) # (glass, half blocks) nospawn_blocks = set([20,44]) -def find_oldimage(chunkXY, cached, cave): - blockid = "%d.%d" % chunkXY - - # Get the name of the existing image. - dir1 = world.base36encode(chunkXY[0]%64) - dir2 = world.base36encode(chunkXY[1]%64) - cachename = '/'.join((dir1, dir2)) - - oldimg = oldimg_path = None - key = ".".join((blockid, "cave" if cave else "nocave")) - if key in cached[cachename]: - oldimg_path = cached[cachename][key] - _, oldimg = os.path.split(oldimg_path) - #logging.debug("Found cached image {0}".format(oldimg)) - return oldimg, oldimg_path - -def check_cache(world, chunkXY, oldimg): - """Returns True is oldimg is OK to use (i.e. not stale)""" -# TODO read to the region file and get the timestamp?? -# TODO currently, just use the mtime on the region file -# TODO (which will cause a single chunk update to invalidate everything in the region - - if not oldimg[1]: return False - chunkfile = os.path.join(world.worlddir, "region", "r.%d.%d.mcr" % (chunkXY[0]//32, chunkXY[1]//32)) - - with open(chunkfile, "rb") as f: - region = nbt.MCRFileReader(f) - mtime = region.get_chunk_timestamp(chunkXY[0], chunkXY[1]) - #logging.debug("checking cache %s against %s %d", chunkfile, oldimg[1], mtime) - try: - if mtime <= os.path.getmtime(oldimg[1]): - return True - return False - except OSError: - return False - # chunkcoords should be the coordinates of a possible chunk. it may not exist -def render_and_save(chunkcoords, cachedir, worldobj, oldimg, cave=False, queue=None): - """Used as the entry point for the multiprocessing workers (since processes - can't target bound methods) or to easily render and save one chunk +def render_to_image(chunkcoords, img, imgcoords, quadtreeobj, cave=False, queue=None): + """Used to render a chunk to a tile in quadtree.py. chunkcoords is a tuple: (chunkX, chunkY) + + imgcoords is as well: (imgX, imgY), which represents the "origin" + to use for drawing. - If the chunk doesn't exist, return None. - Else, returns the image file location""" - a = ChunkRenderer(chunkcoords, cachedir, worldobj, oldimg, queue) + If the chunk doesn't exist, return False. + Else, returns True.""" + a = ChunkRenderer(chunkcoords, quadtreeobj.world, quadtreeobj, queue) try: - return a.render_and_save(cave) + a.chunk_render(img, imgcoords[0], imgcoords[1], cave) + return True except ChunkCorrupt: # This should be non-fatal, but should print a warning pass @@ -191,6 +158,7 @@ def render_and_save(chunkcoords, cachedir, worldobj, oldimg, cave=False, queue=N # entire program, instead of this process dying and the parent waiting # forever for it to finish. raise Exception() + return False class ChunkCorrupt(Exception): pass @@ -199,17 +167,15 @@ class NoSuchChunk(Exception): pass class ChunkRenderer(object): - def __init__(self, chunkcoords, cachedir, worldobj, oldimg, queue): + def __init__(self, chunkcoords, worldobj, quadtreeobj, queue): """Make a new chunk renderer for the given chunk coordinates. chunkcoors should be a tuple: (chunkX, chunkY) cachedir is a directory to save the resulting chunk images to """ self.queue = queue - # derive based on worlddir and chunkcoords - self.regionfile = os.path.join(worldobj.worlddir, "region", - "r.%d.%d.mcr" % (chunkcoords[0] // 32, chunkcoords[1]//32)) - + + self.regionfile = worldobj.get_region_path(*chunkcoords) if not os.path.exists(self.regionfile): raise ValueError("Could not find regionfile: %s" % self.regionfile) @@ -227,23 +193,8 @@ class ChunkRenderer(object): self.chunkX = chunkcoords[0] self.chunkY = chunkcoords[1] - - self.world = worldobj - - - # Cachedir here is the base directory of the caches. We need to go 2 - # levels deeper according to the chunk file. Get the last 2 components - # of destdir and use that - ##moredirs, dir2 = os.path.split(destdir) - ##_, dir1 = os.path.split(moredirs) - self.cachedir = os.path.join(cachedir, - world.base36encode(self.chunkX%64), - world.base36encode(self.chunkY%64)) - - #logging.debug("cache location for this chunk: %s", self.cachedir) - self.oldimg, self.oldimg_path = oldimg - + self.quadtree = quadtreeobj if self.world.useBiomeData: # make sure we've at least *tried* to load the color arrays in this process... @@ -251,15 +202,6 @@ class ChunkRenderer(object): if not textures.grasscolor or not textures.foliagecolor: raise Exception("Can't find grasscolor.png or foliagecolor.png") - - if not os.path.exists(self.cachedir): - try: - os.makedirs(self.cachedir) - except OSError, e: - import errno - if e.errno != errno.EEXIST: - raise - def _load_level(self): """Loads and returns the level structure""" if not hasattr(self, "_level"): @@ -480,69 +422,12 @@ class ChunkRenderer(object): self._digest = digest[:6] return self._digest - def render_and_save(self, cave=False): - """Render the chunk using chunk_render, and then save it to a file in - the same directory as the source image. If the file already exists and - is up to date, this method doesn't render anything. - """ - blockid = self.blockid - - - # Reasons for the code to get to this point: - # 1) An old image doesn't exist - # 2) An old image exists, but the chunk was more recently modified (the - # image was NOT checked if it was valid) - # 3) An old image exists, the chunk was not modified more recently, but - # the image was invalid and deleted (sort of the same as (1)) - - # What /should/ the image be named, go ahead and hash the block array - try: - dest_filename = "img.{0}.{1}.{2}.png".format( - blockid, - "cave" if cave else "nocave", - self._hash_blockarray(), - ) - except NoSuchChunk, e: - return None - - dest_path = os.path.join(self.cachedir, dest_filename) - #logging.debug("cache filename: %s", dest_path) - - if self.oldimg: - if dest_filename == self.oldimg: - # There is an existing file, the chunk has a newer mtime, but the - # hashes match. - # Before we return it, update its mtime so the next round - # doesn't have to check the hash - # TODO confirm hash checking is correct (it should be) - os.utime(dest_path, None) - logging.debug("Using cached image, and updating utime") - return dest_path - else: - # Remove old image for this chunk. Anything already existing is - # either corrupt or out of date - os.unlink(self.oldimg_path) - - - logging.debug("doing a real real render") - # Render the chunk - img = self.chunk_render(cave=cave) - # Save it - try: - img.save(dest_path) - except: - os.unlink(dest_path) - raise - # Return its location - #raise Exception("early exit") - return dest_path - def calculate_darkness(self, skylight, blocklight): """Takes a raw blocklight and skylight, and returns a value between 0.0 (fully lit) and 1.0 (fully black) that can be used as an alpha value for a blend with a black source image. It mimics Minecraft lighting calculations.""" - if not self.world.night: + if not self.quadtree.night: # Daytime return 1.0 - pow(0.8, 15 - max(blocklight, skylight)) else: @@ -662,7 +547,7 @@ class ChunkRenderer(object): # won't get counted as "transparent". blocks = blocks.copy() blocks[self.skylight != 0] = 21 - + blockData = get_blockdata_array(self.level) blockData_expanded = numpy.empty((16,16,128), dtype=numpy.uint8) # Even elements get the lower 4 bits @@ -691,6 +576,11 @@ class ChunkRenderer(object): for x,y,z,imgx,imgy in iterate_chunkblocks(xoff,yoff): blockid = blocks[x,y,z] + + if imgx > img.size[0] + 12 or imgx < -12: + continue + if imgy > img.size[1] + 12 or imgy < -12: + continue # the following blocks don't have textures that can be pre-computed from the blockid # alone. additional data is required. @@ -817,7 +707,7 @@ class ChunkRenderer(object): # no lighting for cave -- depth is probably more useful composite.alpha_over(img, Image.blend(t[0],depth_colors[z],0.3), (imgx, imgy), t[1]) else: - if not self.world.lighting: + if not self.quadtree.lighting: # no lighting at all composite.alpha_over(img, t[0], (imgx, imgy), t[1]) elif blockid in transparent_blocks: @@ -825,7 +715,7 @@ class ChunkRenderer(object): # block shaded with the current # block's light black_coeff, _ = self.get_lighting_coefficient(x, y, z) - if self.world.spawn and black_coeff > 0.8 and blockid in solid_blocks and not ( + if self.quadtree.spawn and black_coeff > 0.8 and blockid in solid_blocks and not ( blockid in nospawn_blocks or ( z != 127 and (blocks[x,y,z+1] in solid_blocks or blocks[x,y,z+1] in fluid_blocks) ) @@ -841,7 +731,7 @@ class ChunkRenderer(object): # top face black_coeff, face_occlude = self.get_lighting_coefficient(x, y, z + 1) # Use red instead of black for spawnable blocks - if self.world.spawn and black_coeff > 0.8 and blockid in solid_blocks and not ( + if self.quadtree.spawn and black_coeff > 0.8 and blockid in solid_blocks and not ( blockid in nospawn_blocks or ( z != 127 and (blocks[x,y,z+1] in solid_blocks or blocks[x,y,z+1] in fluid_blocks) ) @@ -892,13 +782,15 @@ class ChunkRenderer(object): msg=msg, chunk= (self.chunkX, self.chunkY), ) - self.queue.put(["newpoi", newPOI]) + if self.queue: + self.queue.put(["newpoi", newPOI]) # check to see if there are any signs in the persistentData list that are from this chunk. # if so, remove them from the persistentData list (since they're have been added to the world.POI - # list above. - self.queue.put(['removePOI', (self.chunkX, self.chunkY)]) + # list above + if self.queue: + self.queue.put(['removePOI', (self.chunkX, self.chunkY)]) return img diff --git a/quadtree.py b/quadtree.py index aa0a46f..87a52a2 100644 --- a/quadtree.py +++ b/quadtree.py @@ -32,6 +32,7 @@ from time import gmtime, strftime, sleep from PIL import Image import nbt +import chunk from optimizeimages import optimize_image import composite @@ -95,6 +96,11 @@ class QuadtreeGen(object): assert(imgformat) self.imgformat = imgformat self.optimizeimg = optimizeimg + + # TODO placeholders (use config!) + self.lighting = False + self.night = False + self.spawn = False # Make the destination dir if not os.path.exists(destdir): @@ -308,9 +314,8 @@ class QuadtreeGen(object): # Put this in the pool # (even if tilechunks is empty, render_worldtile will delete # existing images if appropriate) - yield pool.apply_async(func=render_worldtile, args= (tilechunks, - colstart, colend, rowstart, rowend, dest, self.imgformat, - self.optimizeimg)) + yield pool.apply_async(func=render_worldtile, args= (self, + tilechunks, colstart, colend, rowstart, rowend, dest)) def _apply_render_inntertile(self, pool, zoom): """Same as _apply_render_worltiles but for the inntertile routine. @@ -530,7 +535,7 @@ def render_innertile(dest, name, imgformat, optimizeimg): optimize_image(imgpath, imgformat, optimizeimg) @catch_keyboardinterrupt -def render_worldtile(chunks, colstart, colend, rowstart, rowend, path, imgformat, optimizeimg): +def render_worldtile(quadtree, chunks, colstart, colend, rowstart, rowend, path): """Renders just the specified chunks into a tile and save it. Unlike usual python conventions, rowend and colend are inclusive. Additionally, the chunks around the edges are half-way cut off (so that neighboring tiles @@ -539,7 +544,7 @@ def render_worldtile(chunks, colstart, colend, rowstart, rowend, path, imgformat chunks is a list of (col, row, chunkx, chunky, filename) of chunk images that are relevant to this call (with their associated regions) - The image is saved to path+".ext" and a hash is saved to path+".hash" + The image is saved to path+"."+quadtree.imgformat If there are no chunks, this tile is not saved (if it already exists, it is deleted) @@ -550,6 +555,7 @@ def render_worldtile(chunks, colstart, colend, rowstart, rowend, path, imgformat There is no return value """ + # width of one chunk is 384. Each column is half a chunk wide. The total # width is (384 + 192*(numcols-1)) since the first column contributes full # width, and each additional one contributes half since they're staggered. @@ -577,10 +583,7 @@ def render_worldtile(chunks, colstart, colend, rowstart, rowend, path, imgformat # way above this one are possibly visible in this tile). Render them # anyways just in case). "chunks" should include up to rowstart-16 - # Before we render any tiles, check the hash of each image in this tile to - # see if it's changed. - # TODO remove hash files? - imgpath = path + "." + imgformat + imgpath = path + "." + quadtree.imgformat # first, remove chunks from `chunks` that don't actually exist in # their region files @@ -643,14 +646,15 @@ def render_worldtile(chunks, colstart, colend, rowstart, rowend, path, imgformat xpos = -192 + (col-colstart)*192 ypos = -96 + (row-rowstart)*96 - # TODO draw chunks! - #composite.alpha_over(tileimg, chunkimg.convert("RGB"), (xpos, ypos), chunkimg) + # draw the chunk! + # TODO cave, queue arguments + chunk.render_to_image((chunkx, chunky), tileimg, (xpos, ypos), quadtree, False, None) # Save them tileimg.save(imgpath) - if optimizeimg: - optimize_image(imgpath, imgformat, optimizeimg) + if quadtree.optimizeimg: + optimize_image(imgpath, quadtree.imgformat, quadtree.optimizeimg) class FakeResult(object): def __init__(self, res):