0
This repository has been archived on 2025-04-25. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Minecraft-Overviewer/overviewer_core/chunk.py
2011-09-24 23:40:06 -04:00

462 lines
18 KiB
Python

# 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 <http://www.gnu.org/licenses/>.
import numpy
from PIL import Image, ImageDraw, ImageEnhance, ImageOps
import os.path
import logging
import time
import math
import sys
import nbt
import textures
import world
import composite
import c_overviewer
"""
This module has routines related to rendering one particular chunk into an
image
"""
# General note about pasting transparent image objects onto an image with an
# alpha channel:
# If you use the image as its own mask, it will work fine only if the alpha
# channel is binary. If there's any translucent parts, then the alpha channel
# of the dest image will have its alpha channel modified. To prevent this:
# first use im.split() and take the third item which is the alpha channel and
# use that as the mask. Then take the image and use im.convert("RGB") to strip
# the image from its alpha channel, and use that as the source to alpha_over()
# (note that this workaround is NOT technically needed when using the
# alpha_over extension, BUT this extension may fall back to PIL's
# paste(), which DOES need the workaround.)
def get_lvldata(world, filename, x, y, retries=2):
"""Takes a filename and chunkcoords and returns the Level struct, which contains all the
level info"""
# non existent region file doesn't mean corrupt chunk.
if filename == None:
raise NoSuchChunk
try:
d = world.load_from_region(filename, x, y)
except Exception, e:
if retries > 0:
# wait a little bit, and try again (up to `retries` times)
time.sleep(1)
#make sure we reload region info
world.reload_region(filename)
return get_lvldata(world, filename, x, y, retries=retries-1)
else:
logging.warning("Error opening chunk (%i, %i) in %s. It may be corrupt. %s", x, y, filename, e)
raise ChunkCorrupt(str(e))
if not d: raise NoSuchChunk(x,y)
return d
def get_blockarray(level):
"""Takes the level struct as returned from get_lvldata, and returns the
Block array, which just contains all the block ids"""
return level['Blocks']
def get_blockarray_fromfile(filename, north_direction='lower-left'):
"""Same as get_blockarray except takes a filename. This is a shortcut"""
d = nbt.load_from_region(filename, x, y, north_direction)
level = d[1]['Level']
chunk_data = level
rots = 0
if self.north_direction == 'upper-left':
rots = 1
elif self.north_direction == 'upper-right':
rots = 2
elif self.north_direction == 'lower-right':
rots = 3
chunk_data['Blocks'] = numpy.rot90(numpy.frombuffer(
level['Blocks'], dtype=numpy.uint8).reshape((16,16,128)),
rots)
return get_blockarray(chunk_data)
def get_skylight_array(level):
"""Returns the skylight array. This is 4 bits per block, but it is
expanded for you so you may index it normally."""
skylight = level['SkyLight']
# this array is 2 blocks per byte, so expand it
skylight_expanded = numpy.empty((16,16,128), dtype=numpy.uint8)
# Even elements get the lower 4 bits
skylight_expanded[:,:,::2] = skylight & 0x0F
# Odd elements get the upper 4 bits
skylight_expanded[:,:,1::2] = (skylight & 0xF0) >> 4
return skylight_expanded
def get_blocklight_array(level):
"""Returns the blocklight array. This is 4 bits per block, but it
is expanded for you so you may index it normally."""
# expand just like get_skylight_array()
blocklight = level['BlockLight']
blocklight_expanded = numpy.empty((16,16,128), dtype=numpy.uint8)
blocklight_expanded[:,:,::2] = blocklight & 0x0F
blocklight_expanded[:,:,1::2] = (blocklight & 0xF0) >> 4
return blocklight_expanded
def get_blockdata_array(level):
"""Returns the ancillary data from the 'Data' byte array. Data is packed
in a similar manner to skylight data"""
return level['Data']
def get_tileentity_data(level):
"""Returns the TileEntities TAG_List from chunk dat file"""
data = level['TileEntities']
return data
# This set holds blocks ids that can be seen through, for occlusion calculations
transparent_blocks = set([ 0, 6, 8, 9, 18, 20, 26, 27, 28, 29, 30, 31, 32, 33,
34, 37, 38, 39, 40, 44, 50, 51, 52, 53, 55, 59, 63, 64,
65, 66, 67, 68, 69, 70, 71, 72, 74, 75, 76, 77, 78, 79,
81, 83, 85, 90, 92, 93, 94, 96, 101, 102, 104, 105,
106, 107, 108, 109])
# This set holds block ids that are solid blocks
solid_blocks = set([1, 2, 3, 4, 5, 7, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22,
23, 24, 25, 35, 41, 42, 43, 44, 45, 46, 47, 48, 49, 53, 54, 56, 57, 58, 60,
61, 62, 67, 73, 74, 78, 79, 80, 81, 82, 84, 86, 87, 88, 89, 91])
# This set holds block ids that are fluid blocks
fluid_blocks = set([8,9,10,11])
# This set holds block ids that are not candidates for spawning mobs on
# (glass, slabs, stairs, fluids, ice, pistons, webs,TNT, wheat, cactus, iron bars, glass planes, fences, fence gate, cake, bed, repeaters, trapdoor)
nospawn_blocks = set([20,26, 29, 30, 33, 34, 44, 46, 53, 59, 67, 79, 81, 85, 92, 93, 94, 96, 107, 109, 101, 102]).union(fluid_blocks)
class ChunkCorrupt(Exception):
pass
class NoSuchChunk(Exception):
pass
class ChunkRenderer(object):
def __init__(self, chunkcoords, worldobj, rendermode, 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
self.regionfile = worldobj.get_region_path(*chunkcoords)
#if not os.path.exists(self.regionfile):
# raise ValueError("Could not find regionfile: %s" % self.regionfile)
## TODO TODO all of this class
#destdir, filename = os.path.split(self.chunkfile)
#filename_split = filename.split(".")
#chunkcoords = filename_split[1:3]
#self.coords = map(world.base36decode, chunkcoords)
#self.blockid = "%d.%d" % chunkcoords
# chunk coordinates (useful to converting local block coords to
# global block coords)
self.chunkX = chunkcoords[0]
self.chunkY = chunkcoords[1]
self.world = worldobj
self.rendermode = rendermode
def _load_level(self):
"""Loads and returns the level structure"""
if not hasattr(self, "_level"):
try:
self._level = get_lvldata(self.world,self.regionfile, self.chunkX, self.chunkY)
except NoSuchChunk, e:
logging.debug("Skipping non-existant chunk")
raise
return self._level
level = property(_load_level)
def _load_blocks(self):
"""Loads and returns the block array"""
if not hasattr(self, "_blocks"):
self._blocks = get_blockarray(self._load_level())
return self._blocks
blocks = property(_load_blocks)
def _load_skylight(self):
"""Loads and returns skylight array"""
if not hasattr(self, "_skylight"):
self._skylight = get_skylight_array(self.level)
return self._skylight
skylight = property(_load_skylight)
def _load_blocklight(self):
"""Loads and returns blocklight array"""
if not hasattr(self, "_blocklight"):
self._blocklight = get_blocklight_array(self.level)
return self._blocklight
blocklight = property(_load_blocklight)
def _load_left(self):
"""Loads and sets data from lower-left chunk"""
chunk_path = self.world.get_region_path(self.chunkX - 1, self.chunkY)
try:
chunk_data = get_lvldata(self.world,chunk_path, self.chunkX - 1, self.chunkY)
self._left_skylight = get_skylight_array(chunk_data)
self._left_blocklight = get_blocklight_array(chunk_data)
self._left_blocks = get_blockarray(chunk_data)
except NoSuchChunk:
self._left_skylight = None
self._left_blocklight = None
self._left_blocks = None
def _load_left_blocks(self):
"""Loads and returns lower-left block array"""
if not hasattr(self, "_left_blocks"):
self._load_left()
return self._left_blocks
left_blocks = property(_load_left_blocks)
def _load_left_skylight(self):
"""Loads and returns lower-left skylight array"""
if not hasattr(self, "_left_skylight"):
self._load_left()
return self._left_skylight
left_skylight = property(_load_left_skylight)
def _load_left_blocklight(self):
"""Loads and returns lower-left blocklight array"""
if not hasattr(self, "_left_blocklight"):
self._load_left()
return self._left_blocklight
left_blocklight = property(_load_left_blocklight)
def _load_right(self):
"""Loads and sets data from lower-right chunk"""
chunk_path = self.world.get_region_path(self.chunkX, self.chunkY + 1)
try:
chunk_data = get_lvldata(self.world,chunk_path, self.chunkX, self.chunkY + 1)
self._right_skylight = get_skylight_array(chunk_data)
self._right_blocklight = get_blocklight_array(chunk_data)
self._right_blocks = get_blockarray(chunk_data)
except NoSuchChunk:
self._right_skylight = None
self._right_blocklight = None
self._right_blocks = None
def _load_right_blocks(self):
"""Loads and returns lower-right block array"""
if not hasattr(self, "_right_blocks"):
self._load_right()
return self._right_blocks
right_blocks = property(_load_right_blocks)
def _load_right_skylight(self):
"""Loads and returns lower-right skylight array"""
if not hasattr(self, "_right_skylight"):
self._load_right()
return self._right_skylight
right_skylight = property(_load_right_skylight)
def _load_right_blocklight(self):
"""Loads and returns lower-right blocklight array"""
if not hasattr(self, "_right_blocklight"):
self._load_right()
return self._right_blocklight
right_blocklight = property(_load_right_blocklight)
def _load_up_right(self):
"""Loads and sets data from upper-right chunk"""
chunk_path = self.world.get_region_path(self.chunkX + 1, self.chunkY)
try:
chunk_data = get_lvldata(self.world,chunk_path, self.chunkX + 1, self.chunkY)
self._up_right_skylight = get_skylight_array(chunk_data)
self._up_right_blocklight = get_blocklight_array(chunk_data)
self._up_right_blocks = get_blockarray(chunk_data)
except NoSuchChunk:
self._up_right_skylight = None
self._up_right_blocklight = None
self._up_right_blocks = None
def _load_up_right_blocks(self):
"""Loads and returns upper-right block array"""
if not hasattr(self, "_up_right_blocks"):
self._load_up_right()
return self._up_right_blocks
up_right_blocks = property(_load_up_right_blocks)
def _load_up_right_skylight(self):
"""Loads and returns lower-right skylight array"""
if not hasattr(self, "_up_right_skylight"):
self._load_up_right()
return self._up_right_skylight
up_right_skylight = property(_load_up_right_skylight)
def _load_up_right_blocklight(self):
"""Loads and returns lower-right blocklight array"""
if not hasattr(self, "_up_right_blocklight"):
self._load_up_right()
return self._up_right_blocklight
up_right_blocklight = property(_load_up_right_blocklight)
def _load_up_left(self):
"""Loads and sets data from upper-left chunk"""
chunk_path = self.world.get_region_path(self.chunkX, self.chunkY - 1)
try:
chunk_data = get_lvldata(self.world,chunk_path, self.chunkX, self.chunkY - 1)
self._up_left_skylight = get_skylight_array(chunk_data)
self._up_left_blocklight = get_blocklight_array(chunk_data)
self._up_left_blocks = get_blockarray(chunk_data)
except NoSuchChunk:
self._up_left_skylight = None
self._up_left_blocklight = None
self._up_left_blocks = None
def _load_up_left_blocks(self):
"""Loads and returns lower-left block array"""
if not hasattr(self, "_up_left_blocks"):
self._load_up_left()
return self._up_left_blocks
up_left_blocks = property(_load_up_left_blocks)
def _load_up_left_skylight(self):
"""Loads and returns lower-right skylight array"""
if not hasattr(self, "_up_left_skylight"):
self._load_up_left()
return self._up_left_skylight
up_left_skylight = property(_load_up_left_skylight)
def _load_up_left_blocklight(self):
"""Loads and returns lower-left blocklight array"""
if not hasattr(self, "_up_left_blocklight"):
self._load_up_left()
return self._up_left_blocklight
up_left_blocklight = property(_load_up_left_blocklight)
def chunk_render(self, img=None, xoff=0, yoff=0, cave=False):
"""Renders a chunk with the given parameters, and returns the image.
If img is given, the chunk is rendered to that image object. Otherwise,
a new one is created. xoff and yoff are offsets in the image.
For cave mode, all blocks that have any direct sunlight are not
rendered, and blocks are drawn with a color tint depending on their
depth."""
blockData = get_blockdata_array(self.level)
blockData_expanded = numpy.empty((16,16,128), dtype=numpy.uint8)
# Even elements get the lower 4 bits
blockData_expanded[:,:,::2] = blockData & 0x0F
# Odd elements get the upper 4 bits
blockData_expanded[:,:,1::2] = blockData >> 4
# Each block is 24x24
# The next block on the X axis adds 12px to x and subtracts 6px from y in the image
# The next block on the Y axis adds 12px to x and adds 6px to y in the image
# The next block up on the Z axis subtracts 12 from y axis in the image
# Since there are 16x16x128 blocks in a chunk, the image will be 384x1728
# (height is 128*12 high, plus the size of the horizontal plane: 16*12)
if not img:
img = Image.new("RGBA", (384, 1728), (38,92,255,0))
c_overviewer.render_loop(self, img, xoff, yoff, blockData_expanded)
tileEntities = get_tileentity_data(self.level)
for entity in tileEntities:
if entity['id'] == 'Sign':
msg=' \n'.join([entity['Text1'], entity['Text2'], entity['Text3'], entity['Text4']])
if msg.strip():
# convert the blockID coordinates from local chunk
# coordinates to global world coordinates
newPOI = dict(type="sign",
x= entity['x'],
y= entity['y'],
z= entity['z'],
msg=msg,
chunk= (self.chunkX, self.chunkY),
)
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.
if self.queue:
self.queue.put(['removePOI', (self.chunkX, self.chunkY)])
return img
# Render 3 blending masks for lighting
# first is top (+Z), second is left (-X), third is right (+Y)
def generate_facemasks():
white = Image.new("L", (24,24), 255)
top = Image.new("L", (24,24), 0)
left = Image.new("L", (24,24), 0)
whole = Image.new("L", (24,24), 0)
toppart = textures.transform_image(white)
leftpart = textures.transform_image_side(white)
# using the real PIL paste here (not alpha_over) because there is
# no alpha channel (and it's mode "L")
top.paste(toppart, (0,0))
left.paste(leftpart, (0,6))
right = left.transpose(Image.FLIP_LEFT_RIGHT)
# Manually touch up 6 pixels that leave a gap, like in
# textures._build_block()
for x,y in [(13,23), (17,21), (21,19)]:
right.putpixel((x,y), 255)
for x,y in [(3,4), (7,2), (11,0)]:
top.putpixel((x,y), 255)
# special fix for chunk boundary stipple
for x,y in [(13,11), (17,9), (21,7)]:
right.putpixel((x,y), 0)
return (top, left, right)
facemasks = generate_facemasks()
black_color = Image.new("RGB", (24,24), (0,0,0))
white_color = Image.new("RGB", (24,24), (255,255,255))
# Render 128 different color images for color coded depth blending in cave mode
def generate_depthcolors():
depth_colors = []
r = 255
g = 0
b = 0
for z in range(128):
depth_colors.append(r)
depth_colors.append(g)
depth_colors.append(b)
if z < 32:
g += 7
elif z < 64:
r -= 7
elif z < 96:
b += 7
else:
g -= 7
return depth_colors
depth_colors = generate_depthcolors()