0

Merge branch 'py-package'

Conflicts:
	setup.py
This commit is contained in:
Aaron Griffith
2011-07-10 18:05:25 -04:00
45 changed files with 198 additions and 71 deletions

View File

491
overviewer_core/chunk.py Normal file
View File

@@ -0,0 +1,491 @@
# 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 numpy.frombuffer(level['Blocks'], dtype=numpy.uint8).reshape((16,16,128))
def get_blockarray_fromfile(filename):
"""Same as get_blockarray except takes a filename. This is a shortcut"""
d = nbt.load_from_region(filename, x, y)
level = d[1]['Level']
return get_blockarray(level)
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 = numpy.frombuffer(level['SkyLight'], dtype=numpy.uint8).reshape((16,16,64))
# 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 = numpy.frombuffer(level['BlockLight'], dtype=numpy.uint8).reshape((16,16,64))
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 numpy.frombuffer(level['Data'], dtype=numpy.uint8).reshape((16,16,64))
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, 30, 31, 32, 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])
# 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, half blocks)
nospawn_blocks = set([20,44])
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_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 generate_pseudo_ancildata(self,x,y,z,blockid, north_position = 0 ):
""" Generates a pseudo ancillary data for blocks that depend of
what are surrounded and don't have ancillary data
This uses a binary number of 4 digits to encode the info.
The encode is:
Bit: 1 2 3 4
Side: x y -x -y
Values: bit = 0 -> The corresponding side block has different blockid
bit = 1 -> The corresponding side block has same blockid
Example: if the bit1 is 1 that means that there is a block with
blockid in the side of the +x direction.
You can rotate the pseudo data multiplying by 2 and
if it is > 15 subtracting 15 and adding 1. (moving bits
in the left direction is like rotate 90 degree in anticlockwise
direction). In this way can be used for maps with other
north orientation.
North position can have the values 0, 1, 2, 3, corresponding to
north in bottom-left, bottom-right, top-right and top-left of
the screen.
The rotation feature is not used anywhere yet.
"""
blocks = self.blocks
up_left_blocks = self.up_left_blocks
up_right_blocks = self.up_right_blocks
left_blocks = self.left_blocks
right_blocks = self.right_blocks
pseudo_data = 0
# first check if we are in the border of a chunk, next check for chunks adjacent to this
# and finally check for a block with same blockid. I we aren't in the border of a chunk,
# check for the block having the sme blockid.
if (up_right_blocks is not None and up_right_blocks[0,y,z] == blockid) if x == 15 else blocks[x+1,y,z] == blockid:
pseudo_data = pseudo_data | 0b1000
if (right_blocks is not None and right_blocks[x,0,z] == blockid) if y == 15 else blocks[x,y + 1,z] == blockid:
pseudo_data = pseudo_data | 0b0100
if (left_blocks is not None and left_blocks[15,y,z] == blockid) if x == 0 else blocks[x - 1,y,z] == blockid:
pseudo_data = pseudo_data | 0b0010
if (up_left_blocks is not None and up_left_blocks[x,15,z] == blockid) if y == 0 else blocks[x,y - 1,z] == blockid:
pseudo_data = pseudo_data | 0b0001
# rotate the bits for other north orientations
while north_position > 0:
pseudo_data *= 2
if pseudo_data > 15:
pseudo_data -= 16
pseudo_data +=1
north_position -= 1
return pseudo_data
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)
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()

View File

@@ -0,0 +1,38 @@
# 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 logging
from PIL import Image
"""
This module has an alpha-over function that is used throughout
Overviewer. It defaults to the PIL paste function when the custom
alpha-over extension cannot be found.
"""
from c_overviewer import alpha_over as extension_alpha_over
def alpha_over(dest, src, pos_or_rect=(0, 0), mask=None):
"""Composite src over dest, using mask as the alpha channel (if
given), otherwise using src's alpha channel. pos_or_rect can
either be a position or a rectangle, specifying where on dest to
put src. Falls back to dest.paste() if the alpha_over extension
can't be found."""
if mask is None:
mask = src
global extension_alpha_over
return extension_alpha_over(dest, src, pos_or_rect, mask)

View File

@@ -0,0 +1,186 @@
from optparse import OptionParser
import sys
import os.path
import logging
class OptionsResults(object):
def get(self, *args):
return self.__dict__.get(*args)
class ConfigOptionParser(object):
def __init__(self, **kwargs):
self.cmdParser = OptionParser(usage=kwargs.get("usage",""))
self.configFile = kwargs.get("config","settings.py")
self.configVars = []
# these are arguments not understood by OptionParser, so they must be removed
# in add_option before being passed to the OptionParser
# note that default is a valid OptionParser argument, but we remove it
# because we want to do our default value handling
self.customArgs = ["required", "commandLineOnly", "default", "listify", "listdelim", "choices"]
self.requiredArgs = []
# add the *very* special config-file path option
self.add_option("--settings", dest="config_file", help="Specifies a settings file to load, by name. This file's format is discussed in the README.", metavar="PATH", type="string", commandLineOnly=True)
def display_config(self):
logging.info("Using the following settings:")
for x in self.configVars:
n = x['dest']
print "%s: %r" % (n, self.configResults.__dict__[n])
def add_option(self, *args, **kwargs):
if kwargs.get("configFileOnly", False) and kwargs.get("commandLineOnly", False):
raise Exception(args, "configFileOnly and commandLineOnly are mututally exclusive")
self.configVars.append(kwargs.copy())
if not kwargs.get("configFileOnly", False):
for arg in self.customArgs:
if arg in kwargs.keys(): del kwargs[arg]
if kwargs.get("type", None):
kwargs['type'] = 'string' # we'll do our own converting later
self.cmdParser.add_option(*args, **kwargs)
def print_help(self):
self.cmdParser.print_help()
def parse_args(self):
# first, load the results from the command line:
options, args = self.cmdParser.parse_args()
# second, use these values to seed the locals dict
l = dict()
g = dict()
for a in self.configVars:
n = a['dest']
if a.get('configFileOnly', False): continue
if a.get('commandLineOnly', False): continue
v = getattr(options, n)
if v != None:
#print "seeding %s with %s" % (n, v)
l[n] = v
else:
# if this has a default, use that to seed the globals dict
if a.get("default", None): g[n] = a['default']
g['args'] = args
try:
if options.config_file:
self.configFile = options.config_file
elif os.path.exists(self.configFile):
# warn about automatic loading
logging.warning("Automatic settings.py loading is DEPRECATED, and may be removed in the future. Please use --settings instead.")
if os.path.exists(self.configFile):
execfile(self.configFile, g, l)
elif options.config_file:
# file does not exist, but *was* specified on the command line
logging.error("Could not open %s." % self.configFile)
sys.exit(1)
except NameError, ex:
import traceback
traceback.print_exc()
logging.error("Error parsing %s. Please check the trackback above" % self.configFile)
sys.exit(1)
except SyntaxError, ex:
import traceback
traceback.print_exc()
tb = sys.exc_info()[2]
#print tb.tb_frame.f_code.co_filename
logging.error("Error parsing %s. Please check the trackback above" % self.configFile)
sys.exit(1)
#print l.keys()
configResults = OptionsResults()
# third, load the results from the config file:
for a in self.configVars:
n = a['dest']
if a.get('commandLineOnly', False):
if n in l.keys():
logging.error("Error: %s can only be specified on the command line. It is not valid in the config file" % n)
sys.exit(1)
configResults.__dict__[n] = l.get(n)
# third, merge options into configReslts (with options overwriting anything in configResults)
for a in self.configVars:
n = a['dest']
if a.get('configFileOnly', False): continue
if getattr(options, n) != None:
configResults.__dict__[n] = getattr(options, n)
# forth, set defaults for any empty values
for a in self.configVars:
n = a['dest']
if (n not in configResults.__dict__.keys() or configResults.__dict__[n] == None) and 'default' in a.keys():
configResults.__dict__[n] = a['default']
# fifth, check required args:
for a in self.configVars:
n = a['dest']
if configResults.__dict__[n] == None and a.get('required',False):
logging.error("%s is required" % n)
sys.exit(1)
# sixth, check types
for a in self.configVars:
n = a['dest']
if 'listify' in a.keys():
# this thing may be a list!
if configResults.__dict__[n] != None and type(configResults.__dict__[n]) == str:
configResults.__dict__[n] = configResults.__dict__[n].split(a.get("listdelim",","))
elif type(configResults.__dict__[n]) != list:
configResults.__dict__[n] = [configResults.__dict__[n]]
if 'type' in a.keys() and configResults.__dict__[n] != None:
try:
configResults.__dict__[n] = self.checkType(configResults.__dict__[n], a)
except ValueError, ex:
logging.error("There was a problem converting the value '%s' to type %s for config parameter '%s'" % (configResults.__dict__[n], a['type'], n))
import traceback
#traceback.print_exc()
sys.exit(1)
self.configResults = configResults
return configResults, args
def checkType(self, value, a):
if type(value) == list:
return map(lambda x: self.checkType(x, a), value)
# switch on type. there are only 7 types that can be used with optparse
if a['type'] == "int":
return int(value)
elif a['type'] == "string":
return str(value)
elif a['type'] == "long":
return long(value)
elif a['type'] == "choice":
if value not in a['choices']:
logging.error("The value '%s' is not valid for config parameter '%s'" % (value, a['dest']))
sys.exit(1)
return value
elif a['type'] == "float":
return long(value)
elif a['type'] == "complex":
return complex(value)
elif a['type'] == "function":
if not callable(value):
raise ValueError("Not callable")
else:
logging.error("Unknown type!")
sys.exit(1)

Binary file not shown.

After

Width:  |  Height:  |  Size: 563 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 401 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 672 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 374 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -0,0 +1,24 @@
<!DOCTYPE html>
<html>
<head>
<meta name="generator" content="Minecraft-Overviewer {version}" />
<meta name="viewport" content="initial-scale=1.0, user-scalable=no" />
<link rel="stylesheet" href="overviewer.css" type="text/css" />
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.3/jquery.min.js"></script>
<script type="text/javascript" src="http://maps.google.com/maps/api/js?sensor=false"></script>
<script type="text/javascript" src="overviewerConfig.js"></script>
<script type="text/javascript" src="overviewer.js"></script>
<script type="text/javascript" src="markers.js"></script>
<script type="text/javascript" src="regions.js"></script>
</head>
<!-- Generated at: {time} -->
<body onload="overviewer.util.initialize()">
<div id="mcmap"></div>
</body>
</html>

View File

@@ -0,0 +1,133 @@
html {
height: 100%;
}
body {
height: 100%;
margin: 0px;
padding: 0px;
background-color: #000;
font-family: Arial, sans-serif;
font-size: 12px;
line-height: 160%;
}
#mcmap {
width: 100%;
height: 100%;
}
.infoWindow {
height: 100px;
}
.infoWindow>img {
width:80px;
float: left;
}
.infoWindow>p {
text-align: center;
font-family: monospace;
}
.customControl {
padding: 5px;
height: 15px;
color: black;
font-family: Arial, sans-serif;
}
.customControl > div.top {
font-size: 12px;
line-height: 160%;
text-align: center;
padding: 0px 6px;
background-image: url('control-bg.png');
background-repeat: repeat-x;
border: 1px solid #A9BBDF;
border-radius: 2px 2px;
box-shadow: rgba(0, 0, 0, 0.347656) 2px 2px 3px;
cursor: pointer;
}
.customControl > div.top:hover {
border: 1px solid #678AC7;
}
.customControl > div.top-active {
color: white;
font-weight: bold;
padding: 0px 5px;
border: 1px solid #678AC7;
background-image: url('control-bg-active.png');
}
.customControl > div.dropDown {
font-size: 12px;
background-color: white;
border: 1px solid #A9BBDF;
border-radius: 2px 2px;
box-shadow: rgba(0, 0, 0, 0.347656) 2px 2px 3px;
display: none;
}
.customControl > div.button {
border: 1px solid #000;
font-size: 12px;
background-color: #fff;
display: none;
}
#link, #coordsDiv {
background-color: #fff; /* fallback */
background-color: rgba(255,255,255,0.55);
border: 1px solid rgb(0, 0, 0);
font-size: 9pt;
padding-left: 2px;
padding-right: 2px;
margin-bottom: 1px;
}
#link:hover {
background-color: #fff; /* fallback */
background-color: rgba(255,255,255,0.8);
}
#searchControl {
padding: 5px;
height: 20px;
font-family: Arial, sans-serif;
}
#searchControl > input {
border: 2px solid #000;
font-size: 12pt;
width: 20em;
background-colour: #fff;
}
div#searchDropDown {
border: 1px solid #000;
width: 17em;
font-size: 14pt;
background-color: #fff;
display: none;
}
div.searchResultItem {
overflow: hidden;
text-overflow: ellipsis;
}
div.searchResultItem img {
width: 24px;
height: 24px;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,157 @@
var overviewerConfig = {
/**
* These are things that will probably not need to be changed by the user,
* but are there because otherwise changing them is a giant PITA.
*/
'CONST': {
/**
* Height and width of the tiles in pixels (I think).
*/
'tileSize': 384,
/**
* Various images used for markers and stuff.
*/
'image': {
'defaultMarker': 'signpost.png',
'signMarker': 'signpost_icon.png',
'compass': 'compass.png',
'spawnMarker': 'http://google-maps-icons.googlecode.com/files/home.png',
'queryMarker': 'http://google-maps-icons.googlecode.com/files/regroup.png'
},
'mapDivId': 'mcmap',
'regionStrokeWeight': 2
},
/**
* General map settings.
*/
'map': {
/**
* Control the visibility of various controls.
*/
'controls': {
/**
* Pan control is the hand with the arrows around it in the upper left.
*/
'pan': true,
/**
* Zoom control is the zoom slider bar in the upper left.
*/
'zoom': true,
/**
* Spawn control is the "Spawn" button that centers the map on spawn.
*/
'spawn': true,
/**
* The compass in the upper right.
*/
'compass': true,
/**
* The mapType control is the slider for selecting different map types.
*/
'mapType': true,
/**
* The small box at the bottom that displays the link to the current map view.
*/
'link': true
},
/**
* The zoom level when the page is loaded without a specific zoom setting
*/
'defaultZoom': 0,
/**
* This controls how far you can zoom out.
*/
'minZoom': {minzoom},
/**
* This controls how close you can zoom in.
*/
'maxZoom': {maxzoom},
/**
* Center on this point, in world coordinates. Should be an array, ex:
* [0,0,0]
*/
'center': {spawn_coords},
/**
* Set this to tell browsers how long they should cache tiles in minutes.
*/
'cacheMinutes': 0,
/**
* Set to true to turn on debug mode, which adds a grid to the map along
* with co-ordinates and a bunch of console output.
*/
'debug': false
},
/**
* Group definitions for objects that are partially selectable (signs and
* regions).
*/
'objectGroups': {
/* signs -- A list of signpost groups. A signpost can fall into zero,
* one, or more than one group. See below for some examples.
*
* Required:
* label : string. Displayed in the drop down menu control.
* match : function. Applied to each marker (from markers.js). It
* is returns true if the marker should be part
* of the group.
*
* Optional:
* checked : boolean. Set to true to have the group visible by default
* icon : string. Used to specify an icon url.
*/
'signs': [
//{label: "'To'", checked: false, match: function(s) {return s.msg.match(/to/)}},
//{label: "Storage", match: function(s) {return s.msg.match(/storage/i) || s.msg.match(/dirt/i) || s.msg.match(/sand/)}},
//{label: "Below Sealevel", match: function(s) { return s.y<64;}},
//{label: "Info", match: function(s) { return s.msg.match("\\[info\\]");}, icon:"http://google-maps-icons.googlecode.com/files/info.png"},
{'label':'All', 'match':function(sign){return true;}}
],
/* regions -- A list of region groups. A region can fall into zero,
* one, or more than one group. See below for some examples.
* Regions have been designed to work with the WorldGuard Overviewer
* Region importer at @link http://goo.gl/dc0tV but your
* host must support php in order to run WG2OvR. You can also continue
* to use any other region format.
*
* Required:
* label : string. Displayed in the drop down menu control.
* clickable : boolean. Will determine if we should generate an
* experimental info window that shows details
* about the clicked region.
* NOTE: if a region (as defined in region.js)
* does not have a label, this will default to
* false.
* match : function. Applied to each region (from region.js). It
* returns true if the region should be part of
* the group.
*
* Optional:
* checked : boolean. Set to true to have the group visible by default
*/
'regions': [
//{'label':'All','clickable':true,'match':function(region){return true;}}
]
},
/* mapTypes -- a list of alternate map renderings available. At least one
* rendering must be listed. When more than one are provided, controls to
* switch between them are provided, with the first one being the default.
*
* Required:
* label : string. Displayed on the control.
* path : string. Location of the rendered tiles.
* Optional:
* base : string. Base of the url path for tile locations, useful
* for serving tiles from a different server than
* the js/html server.
* imgformat : string. File extension used for these tiles. Defaults to png.
* overlay : bool. If true, this tile set will be treated like an overlay
* Example:
* 'mapTypes': [
* {'label': 'Day', 'path': 'lighting/tiles'},
* {'label': 'Night', 'path': 'night/tiles', 'imgformat': 'jpg'},
* {'label': 'Spawn', 'path': 'spawn/tiles', 'base': 'http://example.cdn.amazon.com/'},
* {'label': 'Overlay', 'path': 'overlay/tiles', 'overlay': true}
* ]
*/
'mapTypes': {maptypedata}
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 708 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 253 B

View File

@@ -0,0 +1,192 @@
# 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 os
import os.path
import stat
import cPickle
import Image
import shutil
from time import strftime, localtime
import json
import util
from c_overviewer import get_render_mode_inheritance
import overviewer_version
"""
This module has routines related to generating a Google Maps-based
interface out of a set of tiles.
"""
def mirror_dir(src, dst, entities=None):
'''copies all of the entities from src to dst'''
if not os.path.exists(dst):
os.mkdir(dst)
if entities and type(entities) != list: raise Exception("Expected a list, got a %r instead" % type(entities))
# files which are problematic and should not be copied
# usually, generated by the OS
skip_files = ['Thumbs.db', '.DS_Store']
for entry in os.listdir(src):
if entry in skip_files:
continue
if entities and entry not in entities:
continue
if os.path.isdir(os.path.join(src,entry)):
mirror_dir(os.path.join(src, entry), os.path.join(dst, entry))
elif os.path.isfile(os.path.join(src,entry)):
try:
shutil.copy(os.path.join(src, entry), os.path.join(dst, entry))
except IOError:
# maybe permission problems?
os.chmod(os.path.join(src, entry), stat.S_IRUSR)
os.chmod(os.path.join(dst, entry), stat.S_IWUSR)
shutil.copy(os.path.join(src, entry), os.path.join(dst, entry))
# if this stills throws an error, let it propagate up
class MapGen(object):
def __init__(self, quadtrees, configInfo):
"""Generates a Google Maps interface for the given list of
quadtrees. All of the quadtrees must have the same destdir,
image format, and world.
Note:tiledir for each quadtree should be unique. By default the tiledir is determined by the rendermode"""
self.skipjs = configInfo.get('skipjs', False)
self.nosigns = configInfo.get('nosigns', False)
self.web_assets_hook = configInfo.get('web_assets_hook', None)
self.web_assets_path = configInfo.get('web_assets_path', None)
self.bg_color = configInfo.get('bg_color')
if not len(quadtrees) > 0:
raise ValueError("there must be at least one quadtree to work on")
self.destdir = quadtrees[0].destdir
self.world = quadtrees[0].world
self.p = quadtrees[0].p
for i in quadtrees:
if i.destdir != self.destdir or i.world != self.world:
raise ValueError("all the given quadtrees must have the same destdir and world")
self.quadtrees = quadtrees
def go(self, procs):
"""Writes out config.js, marker.js, and region.js
Copies web assets into the destdir"""
zoomlevel = self.p
bgcolor = (int(self.bg_color[1:3],16), int(self.bg_color[3:5],16), int(self.bg_color[5:7],16), 0)
blank = Image.new("RGBA", (1,1), bgcolor)
# Write a blank image
for quadtree in self.quadtrees:
tileDir = os.path.join(self.destdir, quadtree.tiledir)
if not os.path.exists(tileDir): os.mkdir(tileDir)
blank.save(os.path.join(tileDir, "blank."+quadtree.imgformat))
# copy web assets into destdir:
global_assets = os.path.join(util.get_program_path(), "overviewer_core", "data", "web_assets")
if not os.path.isdir(global_assets):
global_assets = os.path.join(util.get_program_path(), "web_assets")
mirror_dir(global_assets, self.destdir)
# do the same with the local copy, if we have it
if self.web_assets_path:
mirror_dir(self.web_assets_path, self.destdir)
# replace the config js stuff
config = open(os.path.join(self.destdir, 'overviewerConfig.js'), 'r').read()
config = config.replace(
"{minzoom}", str(0))
config = config.replace(
"{maxzoom}", str(zoomlevel))
config = config.replace("{spawn_coords}",
json.dumps(list(self.world.spawn)))
#config = config.replace("{bg_color}", self.bg_color)
# create generated map type data, from given quadtrees
maptypedata = map(lambda q: {'label' : q.rendermode.capitalize(),
'path' : q.tiledir,
'bg_color': self.bg_color,
'overlay' : 'overlay' in get_render_mode_inheritance(q.rendermode),
'imgformat' : q.imgformat},
self.quadtrees)
config = config.replace("{maptypedata}", json.dumps(maptypedata))
with open(os.path.join(self.destdir, "overviewerConfig.js"), 'w') as output:
output.write(config)
# Add time and version in index.html
indexpath = os.path.join(self.destdir, "index.html")
index = open(indexpath, 'r').read()
index = index.replace("{time}", str(strftime("%a, %d %b %Y %H:%M:%S %Z", localtime())))
versionstr = "%s (%s)" % (overviewer_version.VERSION, overviewer_version.HASH[:7])
index = index.replace("{version}", versionstr)
with open(os.path.join(self.destdir, "index.html"), 'w') as output:
output.write(index)
if self.skipjs:
if self.web_assets_hook:
self.web_assets_hook(self)
return
def finalize(self):
if self.skipjs:
return
# since we will only discover PointsOfInterest in chunks that need to be
# [re]rendered, POIs like signs in unchanged chunks will not be listed
# in self.world.POI. To make sure we don't remove these from markers.js
# we need to merge self.world.POI with the persistant data in world.PersistentData
self.world.POI += filter(lambda x: x['type'] != 'spawn', self.world.persistentData['POI'])
if self.nosigns:
markers = filter(lambda x: x['type'] != 'sign', self.world.POI)
else:
markers = self.world.POI
# write out the default marker table
with open(os.path.join(self.destdir, "markers.js"), 'w') as output:
output.write("overviewer.collections.markerDatas.push([\n")
for marker in markers:
output.write(json.dumps(marker))
if marker != markers[-1]:
output.write(",")
output.write("\n")
output.write("]);\n")
# save persistent data
self.world.persistentData['POI'] = self.world.POI
with open(self.world.pickleFile,"wb") as f:
cPickle.dump(self.world.persistentData,f)
# write out the default (empty, but documented) region table
with open(os.path.join(self.destdir, "regions.js"), 'w') as output:
output.write('overviewer.collections.regionDatas.push([\n')
output.write(' // {"color": "#FFAA00", "opacity": 0.5, "closed": true, "path": [\n')
output.write(' // {"x": 0, "y": 0, "z": 0},\n')
output.write(' // {"x": 0, "y": 10, "z": 0},\n')
output.write(' // {"x": 0, "y": 0, "z": 10}\n')
output.write(' // ]},\n')
output.write(']);')

404
overviewer_core/nbt.py Normal file
View File

@@ -0,0 +1,404 @@
# 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 gzip, zlib
import struct
import StringIO
import os
# decorator to handle filename or object as first parameter
def _file_loader(func):
def wrapper(fileobj, *args):
if isinstance(fileobj, basestring):
if not os.path.isfile(fileobj):
return None
# Is actually a filename
fileobj = open(fileobj, 'rb',4096)
return func(fileobj, *args)
return wrapper
@_file_loader
def load(fileobj):
return NBTFileReader(fileobj).read_all()
def load_from_region(filename, x, y):
nbt = load_region(filename).load_chunk(x, y)
if nbt is None:
return None ## return none. I think this is who we should indicate missing chunks
#raise IOError("No such chunk in region: (%i, %i)" % (x, y))
return nbt.read_all()
def load_region(filename):
return MCRFileReader(filename)
# compile the unpacker's into a classes
_byte = struct.Struct("b")
_short = struct.Struct(">h")
_int = struct.Struct(">i")
_long = struct.Struct(">q")
_float = struct.Struct(">f")
_double = struct.Struct(">d")
_24bit_int = struct.Struct("B B B")
_unsigned_byte = struct.Struct("B")
_unsigned_int = struct.Struct(">I")
_chunk_header = struct.Struct(">I B")
class NBTFileReader(object):
def __init__(self, fileobj, is_gzip=True):
if is_gzip:
self._file = gzip.GzipFile(fileobj=fileobj, mode='rb')
else:
# pure zlib stream -- maybe later replace this with
# a custom zlib file object?
data = zlib.decompress(fileobj.read())
self._file = StringIO.StringIO(data)
# These private methods read the payload only of the following types
def _read_tag_end(self):
# Nothing to read
return 0
def _read_tag_byte(self):
byte = self._file.read(1)
return _byte.unpack(byte)[0]
def _read_tag_short(self):
bytes = self._file.read(2)
global _short
return _short.unpack(bytes)[0]
def _read_tag_int(self):
bytes = self._file.read(4)
global _int
return _int.unpack(bytes)[0]
def _read_tag_long(self):
bytes = self._file.read(8)
global _long
return _long.unpack(bytes)[0]
def _read_tag_float(self):
bytes = self._file.read(4)
global _float
return _float.unpack(bytes)[0]
def _read_tag_double(self):
bytes = self._file.read(8)
global _double
return _double.unpack(bytes)[0]
def _read_tag_byte_array(self):
length = self._read_tag_int()
bytes = self._file.read(length)
return bytes
def _read_tag_string(self):
length = self._read_tag_short()
# Read the string
string = self._file.read(length)
# decode it and return
return string.decode("UTF-8")
def _read_tag_list(self):
tagid = self._read_tag_byte()
length = self._read_tag_int()
read_tagmap = {
0: self._read_tag_end,
1: self._read_tag_byte,
2: self._read_tag_short,
3: self._read_tag_int,
4: self._read_tag_long,
5: self._read_tag_float,
6: self._read_tag_double,
7: self._read_tag_byte_array,
8: self._read_tag_string,
9: self._read_tag_list,
10:self._read_tag_compound,
}
read_method = read_tagmap[tagid]
l = []
for _ in xrange(length):
l.append(read_method())
return l
def _read_tag_compound(self):
# Build a dictionary of all the tag names mapping to their payloads
tags = {}
while True:
# Read a tag
tagtype = ord(self._file.read(1))
if tagtype == 0:
break
name = self._read_tag_string()
read_tagmap = {
0: self._read_tag_end,
1: self._read_tag_byte,
2: self._read_tag_short,
3: self._read_tag_int,
4: self._read_tag_long,
5: self._read_tag_float,
6: self._read_tag_double,
7: self._read_tag_byte_array,
8: self._read_tag_string,
9: self._read_tag_list,
10:self._read_tag_compound,
}
payload = read_tagmap[tagtype]()
tags[name] = payload
return tags
def read_all(self):
"""Reads the entire file and returns (name, payload)
name is the name of the root tag, and payload is a dictionary mapping
names to their payloads
"""
# Read tag type
tagtype = ord(self._file.read(1))
if tagtype != 10:
raise Exception("Expected a tag compound")
# Read the tag name
name = self._read_tag_string()
payload = self._read_tag_compound()
return name, payload
# For reference, the MCR format is outlined at
# <http://www.minecraftwiki.net/wiki/Beta_Level_Format>
class MCRFileReader(object):
"""A class for reading chunk region files, as introduced in the
Beta 1.3 update. It provides functions for opening individual
chunks (as instances of NBTFileReader), getting chunk timestamps,
and for listing chunks contained in the file."""
def __init__(self, filename):
self._file = None
self._filename = filename
# cache used when the entire header tables are read in get_chunks()
self._locations = None
self._timestamps = None
self._chunks = None
def _read_24bit_int(self):
"""Read in a 24-bit, big-endian int, used in the chunk
location table."""
ret = 0
bytes = self._file.read(3)
global _24bit_int
bytes = _24bit_int.unpack(bytes)
for i in xrange(3):
ret = ret << 8
ret += bytes[i]
return ret
def _read_chunk_location(self, x=None, y=None):
"""Read and return the (offset, length) of the given chunk
coordinate, or None if the requested chunk doesn't exist. x
and y must be between 0 and 31, or None. If they are None,
then there will be no file seek before doing the read."""
if x is not None and y is not None:
if (not x >= 0) or (not x < 32) or (not y >= 0) or (not y < 32):
raise ValueError("Chunk location out of range.")
# check for a cached value
if self._locations:
return self._locations[x + y * 32]
# go to the correct entry in the chunk location table
self._file.seek(4 * (x + y * 32))
try:
# 3-byte offset in 4KiB sectors
offset_sectors = self._read_24bit_int()
# 1-byte length in 4KiB sectors, rounded up
global _unsigned_byte
byte = self._file.read(1)
length_sectors = _unsigned_byte.unpack(byte)[0]
except (IndexError, struct.error):
# got a problem somewhere
return None
# check for empty chunks
if offset_sectors == 0 or length_sectors == 0:
return None
return (offset_sectors * 4096, length_sectors * 4096)
def _read_chunk_timestamp(self, x=None, y=None):
"""Read and return the last modification time of the given
chunk coordinate. x and y must be between 0 and 31, or
None. If they are, None, then there will be no file seek
before doing the read."""
if x is not None and y is not None:
if (not x >= 0) or (not x < 32) or (not y >= 0) or (not y < 32):
raise ValueError("Chunk location out of range.")
# check for a cached value
if self._timestamps:
return self._timestamps[x + y * 32]
# go to the correct entry in the chunk timestamp table
self._file.seek(4 * (x + y * 32) + 4096)
try:
bytes = self._file.read(4)
global _unsigned_int
timestamp = _unsigned_int.unpack(bytes)[0]
except (IndexError, struct.error):
return 0
return timestamp
def openfile(self):
#make sure we clean up
if self._file is None:
self._file = open(self._filename,'rb')
def closefile(self):
#make sure we clean up
if self._file is not None:
self._file.close()
self._file = None
def get_chunks(self):
"""Return a list of all chunks contained in this region file,
as a list of (x, y) coordinate tuples. To load these chunks,
provide these coordinates to load_chunk()."""
if self._chunks is not None:
return self._chunks
if self._locations is None:
self.get_chunk_info()
self._chunks = []
for x in xrange(32):
for y in xrange(32):
if self._locations[x + y * 32] is not None:
self._chunks.append((x,y))
return self._chunks
def get_chunk_info(self,closeFile = True):
"""Preloads region header information."""
if self._locations:
return
self.openfile()
self._chunks = None
self._locations = []
self._timestamps = []
# go to the beginning of the file
self._file.seek(0)
# read chunk location table
locations_append = self._locations.append
for _ in xrange(32*32):
locations_append(self._read_chunk_location())
# read chunk timestamp table
timestamp_append = self._timestamps.append
for _ in xrange(32*32):
timestamp_append(self._read_chunk_timestamp())
if closeFile:
self.closefile()
return
def get_chunk_timestamp(self, x, y):
"""Return the given chunk's modification time. If the given
chunk doesn't exist, this number may be nonsense. Like
load_chunk(), this will wrap x and y into the range [0, 31].
"""
x = x % 32
y = y % 32
if self._timestamps is None:
self.get_chunk_info()
return self._timestamps[x + y * 32]
def chunkExists(self, x, y):
"""Determines if a chunk exists without triggering loading of the backend data"""
x = x % 32
y = y % 32
if self._locations is None:
self.get_chunk_info()
location = self._locations[x + y * 32]
return location is not None
def load_chunk(self, x, y,closeFile=True):
"""Return a NBTFileReader instance for the given chunk, or
None if the given chunk doesn't exist in this region file. If
you provide an x or y not between 0 and 31, it will be
modulo'd into this range (x % 32, etc.) This is so you can
provide chunk coordinates in global coordinates, and still
have the chunks load out of regions properly."""
x = x % 32
y = y % 32
if self._locations is None:
self.get_chunk_info()
location = self._locations[x + y * 32]
if location is None:
return None
self.openfile()
# seek to the data
self._file.seek(location[0])
# read in the chunk data header
bytes = self._file.read(5)
data_length,compression = _chunk_header.unpack(bytes)
# figure out the compression
is_gzip = True
if compression == 1:
# gzip -- not used by the official client, but trivial to support here so...
is_gzip = True
elif compression == 2:
# deflate -- pure zlib stream
is_gzip = False
else:
# unsupported!
raise Exception("Unsupported chunk compression type: %i" % (compression))
# turn the rest of the data into a StringIO object
# (using data_length - 1, as we already read 1 byte for compression)
data = self._file.read(data_length - 1)
data = StringIO.StringIO(data)
if closeFile:
self.closefile()
return NBTFileReader(data, is_gzip=is_gzip)

View File

@@ -0,0 +1,52 @@
# 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 os
import subprocess
import shlex
pngcrush = "pngcrush"
optipng = "optipng"
advdef = "advdef"
def check_programs(level):
path = os.environ.get("PATH").split(os.pathsep)
def exists_in_path(prog):
result = filter(lambda x: os.path.exists(os.path.join(x, prog)), path)
return len(result) != 0
for prog,l in [(pngcrush,1), (advdef,2)]:
if l <= level:
if (not exists_in_path(prog)) and (not exists_in_path(prog + ".exe")):
raise Exception("Optimization prog %s for level %d not found!" % (prog, l))
def optimize_image(imgpath, imgformat, optimizeimg):
if imgformat == 'png':
if optimizeimg >= 1:
# we can't do an atomic replace here because windows is terrible
# so instead, we make temp files, delete the old ones, and rename
# the temp files. go windows!
subprocess.Popen([pngcrush, imgpath, imgpath + ".tmp"],
stderr=subprocess.STDOUT, stdout=subprocess.PIPE).communicate()[0]
os.remove(imgpath)
os.rename(imgpath+".tmp", imgpath)
if optimizeimg >= 2:
# the "-nc" it's needed to no broke the transparency of tiles
recompress_option = "-z2" if optimizeimg == 2 else "-z4"
subprocess.Popen([advdef, recompress_option,imgpath], stderr=subprocess.STDOUT,
stdout=subprocess.PIPE).communicate()[0]

484
overviewer_core/quadtree.py Normal file
View File

@@ -0,0 +1,484 @@
# 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 multiprocessing
import itertools
import os
import os.path
import functools
import re
import shutil
import collections
import json
import logging
import util
import cPickle
import stat
import errno
import time
from time import gmtime, strftime, sleep
from PIL import Image
import nbt
import chunk
from c_overviewer import get_render_mode_inheritance
from optimizeimages import optimize_image
import composite
"""
This module has routines related to generating a quadtree of tiles
"""
def iterate_base4(d):
"""Iterates over a base 4 number with d digits"""
return itertools.product(xrange(4), repeat=d)
class QuadtreeGen(object):
def __init__(self, worldobj, destdir, bgcolor, depth=None, tiledir=None, forcerender=False, imgformat=None, imgquality=95, optimizeimg=None, rendermode="normal"):
"""Generates a quadtree from the world given into the
given dest directory
worldobj is a world.WorldRenderer object that has already been processed
If depth is given, it overrides the calculated value. Otherwise, the
minimum depth that contains all chunks is calculated and used.
"""
assert(imgformat)
self.forcerender = forcerender
self.imgformat = imgformat
self.imgquality = imgquality
self.optimizeimg = optimizeimg
self.bgcolor = bgcolor
self.rendermode = rendermode
# force png renderformat if we're using an overlay mode
if 'overlay' in get_render_mode_inheritance(rendermode):
self.imgformat = "png"
# Make the destination dir
if not os.path.exists(destdir):
os.makedirs(os.path.abspath(destdir))
if tiledir is None:
tiledir = rendermode
self.tiledir = tiledir
if depth is None:
# Determine quadtree depth (midpoint is always 0,0)
for p in xrange(15):
# Will 2^p tiles wide and high suffice?
# X has twice as many chunks as tiles, then halved since this is a
# radius
xradius = 2**p
# Y has 4 times as many chunks as tiles, then halved since this is
# a radius
yradius = 2*2**p
if xradius >= worldobj.maxcol and -xradius <= worldobj.mincol and \
yradius >= worldobj.maxrow and -yradius <= worldobj.minrow:
break
else:
raise ValueError("Your map is waaaay too big! Use the 'zoom' option in 'settings.py'.")
self.p = p
else:
self.p = depth
xradius = 2**depth
yradius = 2*2**depth
# Make new row and column ranges
self.mincol = -xradius
self.maxcol = xradius
self.minrow = -yradius
self.maxrow = yradius
self.world = worldobj
self.destdir = destdir
self.full_tiledir = os.path.join(destdir, tiledir)
def _get_cur_depth(self):
"""How deep is the quadtree currently in the destdir? This glances in
config.js to see what maxZoom is set to.
returns -1 if it couldn't be detected, file not found, or nothing in
config.js matched
"""
indexfile = os.path.join(self.destdir, "overviewerConfig.js")
if not os.path.exists(indexfile):
return -1
matcher = re.compile(r"maxZoom.*:\s*(\d+)")
p = -1
for line in open(indexfile, "r"):
res = matcher.search(line)
if res:
p = int(res.group(1))
break
return p
def _increase_depth(self):
"""Moves existing tiles into place for a larger tree"""
getpath = functools.partial(os.path.join, self.destdir, self.tiledir)
# At top level of the tree:
# quadrant 0 is now 0/3
# 1 is now 1/2
# 2 is now 2/1
# 3 is now 3/0
# then all that needs to be done is to regenerate the new top level
for dirnum in range(4):
newnum = (3,2,1,0)[dirnum]
newdir = "new" + str(dirnum)
newdirpath = getpath(newdir)
files = [str(dirnum)+"."+self.imgformat, str(dirnum)]
newfiles = [str(newnum)+"."+self.imgformat, str(newnum)]
os.mkdir(newdirpath)
for f, newf in zip(files, newfiles):
p = getpath(f)
if os.path.exists(p):
os.rename(p, getpath(newdir, newf))
os.rename(newdirpath, getpath(str(dirnum)))
def _decrease_depth(self):
"""If the map size decreases, or perhaps the user has a depth override
in effect, re-arrange existing tiles for a smaller tree"""
getpath = functools.partial(os.path.join, self.destdir, self.tiledir)
# quadrant 0/3 goes to 0
# 1/2 goes to 1
# 2/1 goes to 2
# 3/0 goes to 3
# Just worry about the directories here, the files at the top two
# levels are cheap enough to replace
if os.path.exists(getpath("0", "3")):
os.rename(getpath("0", "3"), getpath("new0"))
shutil.rmtree(getpath("0"))
os.rename(getpath("new0"), getpath("0"))
if os.path.exists(getpath("1", "2")):
os.rename(getpath("1", "2"), getpath("new1"))
shutil.rmtree(getpath("1"))
os.rename(getpath("new1"), getpath("1"))
if os.path.exists(getpath("2", "1")):
os.rename(getpath("2", "1"), getpath("new2"))
shutil.rmtree(getpath("2"))
os.rename(getpath("new2"), getpath("2"))
if os.path.exists(getpath("3", "0")):
os.rename(getpath("3", "0"), getpath("new3"))
shutil.rmtree(getpath("3"))
os.rename(getpath("new3"), getpath("3"))
def go(self, procs):
"""Processing before tile rendering"""
curdepth = self._get_cur_depth()
if curdepth != -1:
if self.p > curdepth:
logging.warning("Your map seemes to have expanded beyond its previous bounds.")
logging.warning( "Doing some tile re-arrangements... just a sec...")
for _ in xrange(self.p-curdepth):
self._increase_depth()
elif self.p < curdepth:
logging.warning("Your map seems to have shrunk. Re-arranging tiles, just a sec...")
for _ in xrange(curdepth - self.p):
self._decrease_depth()
def _get_range_by_path(self, path):
"""Returns the x, y chunk coordinates of this tile"""
x, y = self.mincol, self.minrow
xsize = self.maxcol
ysize = self.maxrow
for p in path:
if p in (1, 3):
x += xsize
if p in (2, 3):
y += ysize
xsize //= 2
ysize //= 2
return x, y
def get_chunks_in_range(self, colstart, colend, rowstart, rowend):
"""Get chunks that are relevant to the tile rendering function that's
rendering that range"""
chunklist = []
unconvert_coords = self.world.unconvert_coords
#get_region_path = self.world.get_region_path
get_region = self.world.regionfiles.get
regionx = None
regiony = None
c = None
mcr = None
for row in xrange(rowstart-16, rowend+1):
for col in xrange(colstart, colend+1):
# due to how chunks are arranged, we can only allow
# even row, even column or odd row, odd column
# otherwise, you end up with duplicates!
if row % 2 != col % 2:
continue
chunkx, chunky = unconvert_coords(col, row)
regionx_ = chunkx//32
regiony_ = chunky//32
if regionx_ != regionx or regiony_ != regiony:
regionx = regionx_
regiony = regiony_
_, _, c, mcr = get_region((regionx, regiony),(None,None,None,None))
if c is not None and mcr.chunkExists(chunkx,chunky):
chunklist.append((col, row, chunkx, chunky, c))
return chunklist
def get_worldtiles(self):
"""Returns an iterator over the tiles of the most detailed layer
"""
for path in iterate_base4(self.p):
# Get the range for this tile
colstart, rowstart = self._get_range_by_path(path)
colend = colstart + 2
rowend = rowstart + 4
# This image is rendered at(relative to the worker's destdir):
tilepath = [str(x) for x in path]
tilepath = os.sep.join(tilepath)
#logging.debug("this is rendered at %s", dest)
# Put this in the batch to be submited to the pool
yield [self,colstart, colend, rowstart, rowend, tilepath]
def get_innertiles(self,zoom):
"""Same as get_worldtiles but for the inntertile routine.
"""
for path in iterate_base4(zoom):
# This image is rendered at(relative to the worker's destdir):
tilepath = [str(x) for x in path[:-1]]
tilepath = os.sep.join(tilepath)
name = str(path[-1])
yield [self,tilepath, name]
def render_innertile(self, dest, name):
"""
Renders a tile at os.path.join(dest, name)+".ext" by taking tiles from
os.path.join(dest, name, "{0,1,2,3}.png")
"""
imgformat = self.imgformat
imgpath = os.path.join(dest, name) + "." + imgformat
if name == "base":
quadPath = [[(0,0),os.path.join(dest, "0." + imgformat)],[(192,0),os.path.join(dest, "1." + imgformat)], [(0, 192),os.path.join(dest, "2." + imgformat)],[(192,192),os.path.join(dest, "3." + imgformat)]]
else:
quadPath = [[(0,0),os.path.join(dest, name, "0." + imgformat)],[(192,0),os.path.join(dest, name, "1." + imgformat)],[(0, 192),os.path.join(dest, name, "2." + imgformat)],[(192,192),os.path.join(dest, name, "3." + imgformat)]]
#stat the tile, we need to know if it exists or it's mtime
try:
tile_mtime = os.stat(imgpath)[stat.ST_MTIME];
except OSError, e:
if e.errno != errno.ENOENT:
raise
tile_mtime = None
#check mtimes on each part of the quad, this also checks if they exist
needs_rerender = tile_mtime is None
quadPath_filtered = []
for path in quadPath:
try:
quad_mtime = os.stat(path[1])[stat.ST_MTIME];
quadPath_filtered.append(path)
if quad_mtime > tile_mtime:
needs_rerender = True
except OSError:
# We need to stat all the quad files, so keep looping
pass
# do they all not exist?
if quadPath_filtered == []:
if tile_mtime is not None:
os.unlink(imgpath)
return
# quit now if we don't need rerender
if not needs_rerender:
return
#logging.debug("writing out innertile {0}".format(imgpath))
# Create the actual image now
img = Image.new("RGBA", (384, 384), self.bgcolor)
# we'll use paste (NOT alpha_over) for quadtree generation because
# this is just straight image stitching, not alpha blending
for path in quadPath_filtered:
try:
quad = Image.open(path[1]).resize((192,192), Image.ANTIALIAS)
img.paste(quad, path[0])
except Exception, e:
logging.warning("Couldn't open %s. It may be corrupt, you may need to delete it. %s", path[1], e)
# Save it
if self.imgformat == 'jpg':
img.save(imgpath, quality=self.imgquality, subsampling=0)
else: # png
img.save(imgpath)
if self.optimizeimg:
optimize_image(imgpath, self.imgformat, self.optimizeimg)
def render_worldtile(self, chunks, colstart, colend, rowstart, rowend, path, poi_queue=None):
"""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
will render the other half)
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+"."+self.imgformat
If there are no chunks, this tile is not saved (if it already exists, it is
deleted)
Standard tile size has colend-colstart=2 and rowend-rowstart=4
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.
# However, since we want to cut off half a chunk at each end (384 less
# pixels) and since (colend - colstart + 1) is the number of columns
# inclusive, the equation simplifies to:
width = 192 * (colend - colstart)
# Same deal with height
height = 96 * (rowend - rowstart)
# The standard tile size is 3 columns by 5 rows, which works out to 384x384
# pixels for 8 total chunks. (Since the chunks are staggered but the grid
# is not, some grid coordinates do not address chunks) The two chunks on
# the middle column are shown in full, the two chunks in the middle row are
# half cut off, and the four remaining chunks are one quarter shown.
# The above example with cols 0-3 and rows 0-4 has the chunks arranged like this:
# 0,0 2,0
# 1,1
# 0,2 2,2
# 1,3
# 0,4 2,4
# Due to how the tiles fit together, we may need to render chunks way above
# this (since very few chunks actually touch the top of the sky, some tiles
# way above this one are possibly visible in this tile). Render them
# anyways just in case). "chunks" should include up to rowstart-16
imgpath = path + "." + self.imgformat
world = self.world
#stat the file, we need to know if it exists or it's mtime
try:
tile_mtime = os.stat(imgpath)[stat.ST_MTIME];
except OSError, e:
if e.errno != errno.ENOENT:
raise
tile_mtime = None
if not chunks:
# No chunks were found in this tile
if tile_mtime is not None:
os.unlink(imgpath)
return None
# Create the directory if not exists
dirdest = os.path.dirname(path)
if not os.path.exists(dirdest):
try:
os.makedirs(dirdest)
except OSError, e:
# Ignore errno EEXIST: file exists. Since this is multithreaded,
# two processes could conceivably try and create the same directory
# at the same time.
if e.errno != errno.EEXIST:
raise
# check chunk mtimes to see if they are newer
try:
needs_rerender = False
get_region_mtime = world.get_region_mtime
for col, row, chunkx, chunky, regionfile in chunks:
# bail early if forcerender is set
if self.forcerender:
needs_rerender = True
break
# check region file mtime first.
region,regionMtime = get_region_mtime(regionfile)
if regionMtime <= tile_mtime:
continue
# don't even check if it's not in the regionlist
if self.world.regionlist and os.path.abspath(region._filename) not in self.world.regionlist:
continue
# checking chunk mtime
if region.get_chunk_timestamp(chunkx, chunky) > tile_mtime:
needs_rerender = True
break
# if after all that, we don't need a rerender, return
if not needs_rerender:
return None
except OSError:
# couldn't get tile mtime, skip check
pass
#logging.debug("writing out worldtile {0}".format(imgpath))
# Compile this image
tileimg = Image.new("RGBA", (width, height), self.bgcolor)
world = self.world
rendermode = self.rendermode
# col colstart will get drawn on the image starting at x coordinates -(384/2)
# row rowstart will get drawn on the image starting at y coordinates -(192/2)
for col, row, chunkx, chunky, regionfile in chunks:
xpos = -192 + (col-colstart)*192
ypos = -96 + (row-rowstart)*96
# draw the chunk!
try:
a = chunk.ChunkRenderer((chunkx, chunky), world, rendermode, poi_queue)
a.chunk_render(tileimg, xpos, ypos, None)
except chunk.ChunkCorrupt:
# an error was already printed
pass
# Save them
if self.imgformat == 'jpg':
tileimg.save(imgpath, quality=self.imgquality, subsampling=0)
else: # png
tileimg.save(imgpath)
if self.optimizeimg:
optimize_image(imgpath, self.imgformat, self.optimizeimg)

View File

@@ -0,0 +1,377 @@
# 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 multiprocessing
import Queue
import itertools
from itertools import cycle, islice
import os
import os.path
import functools
import re
import shutil
import collections
import json
import logging
import util
import textures
import c_overviewer
import cPickle
import stat
import errno
import time
from time import gmtime, strftime, sleep
"""
This module has routines related to distributing the render job to multipule nodes
"""
def catch_keyboardinterrupt(func):
"""Decorator that catches a keyboardinterrupt and raises a real exception
so that multiprocessing will propagate it properly"""
@functools.wraps(func)
def newfunc(*args, **kwargs):
try:
return func(*args, **kwargs)
except KeyboardInterrupt:
logging.error("Ctrl-C caught!")
raise Exception("Exiting")
except:
import traceback
traceback.print_exc()
raise
return newfunc
child_rendernode = None
def pool_initializer(rendernode):
logging.debug("Child process {0}".format(os.getpid()))
#stash the quadtree objects in a global variable after fork() for windows compat.
global child_rendernode
child_rendernode = rendernode
# make sure textures are generated for this process
# and initialize c_overviewer
textures.generate(path=rendernode.options.get('textures_path', None))
c_overviewer.init_chunk_render()
# load biome data in each process, if needed
for quadtree in rendernode.quadtrees:
if quadtree.world.useBiomeData:
# make sure we've at least *tried* to load the color arrays in this process...
textures.prepareBiomeData(quadtree.world.worlddir)
if not textures.grasscolor or not textures.foliagecolor:
raise Exception("Can't find grasscolor.png or foliagecolor.png")
# only load biome data once
break
#http://docs.python.org/library/itertools.html
def roundrobin(iterables):
"roundrobin('ABC', 'D', 'EF') --> A D E B F C"
# Recipe credited to George Sakkis
pending = len(iterables)
nexts = cycle(iter(it).next for it in iterables)
while pending:
try:
for next in nexts:
yield next()
except StopIteration:
pending -= 1
nexts = cycle(islice(nexts, pending))
class RenderNode(object):
def __init__(self, quadtrees, options):
"""Distributes the rendering of a list of quadtrees."""
if not len(quadtrees) > 0:
raise ValueError("there must be at least one quadtree to work on")
self.options = options
self.quadtrees = quadtrees
#bind an index value to the quadtree so we can find it again
#and figure out which worlds are where
i = 0
self.worlds = []
for q in quadtrees:
q._render_index = i
i += 1
if q.world not in self.worlds:
self.worlds.append(q.world)
manager = multiprocessing.Manager()
# queue for receiving interesting events from the renderer
# (like the discovery of signs!
#stash into the world object like we stash an index into the quadtree
for world in self.worlds:
world.poi_q = manager.Queue()
def print_statusline(self, complete, total, level, unconditional=False):
if unconditional:
pass
elif complete < 100:
if not complete % 25 == 0:
return
elif complete < 1000:
if not complete % 100 == 0:
return
else:
if not complete % 1000 == 0:
return
logging.info("{0}/{1} tiles complete on level {2}/{3}".format(
complete, total, level, self.max_p))
def go(self, procs):
"""Renders all tiles"""
logging.debug("Parent process {0}".format(os.getpid()))
# Create a pool
if procs == 1:
pool = FakePool()
pool_initializer(self)
else:
pool = multiprocessing.Pool(processes=procs,initializer=pool_initializer,initargs=(self,))
#warm up the pool so it reports all the worker id's
if logging.getLogger().level >= 10:
pool.map(bool,xrange(multiprocessing.cpu_count()),1)
else:
pool.map_async(bool,xrange(multiprocessing.cpu_count()),1)
quadtrees = self.quadtrees
# do per-quadtree init.
max_p = 0
total = 0
for q in quadtrees:
total += 4**q.p
if q.p > max_p:
max_p = q.p
self.max_p = max_p
# Render the highest level of tiles from the chunks
results = collections.deque()
complete = 0
logging.info("Rendering highest zoom level of tiles now.")
logging.info("Rendering {0} layer{1}".format(len(quadtrees),'s' if len(quadtrees) > 1 else '' ))
logging.info("There are {0} tiles to render".format(total))
logging.info("There are {0} total levels to render".format(self.max_p))
logging.info("Don't worry, each level has only 25% as many tiles as the last.")
logging.info("The others will go faster")
count = 0
batch_size = 4*len(quadtrees)
while batch_size < 10:
batch_size *= 2
timestamp = time.time()
for result in self._apply_render_worldtiles(pool,batch_size):
results.append(result)
# every second drain some of the queue
timestamp2 = time.time()
if timestamp2 >= timestamp + 1:
timestamp = timestamp2
count_to_remove = (1000//batch_size)
if count_to_remove < len(results):
for world in self.worlds:
try:
while (1):
# an exception will break us out of this loop
item = world.poi_q.get(block=False)
if item[0] == "newpoi":
if item[1] not in world.POI:
#print "got an item from the queue!"
world.POI.append(item[1])
elif item[0] == "removePOI":
world.persistentData['POI'] = filter(lambda x: x['chunk'] != item[1], world.persistentData['POI'])
except Queue.Empty:
pass
while count_to_remove > 0:
count_to_remove -= 1
complete += results.popleft().get()
self.print_statusline(complete, total, 1)
if len(results) > (10000//batch_size):
# Empty the queue before adding any more, so that memory
# required has an upper bound
while len(results) > (500//batch_size):
complete += results.popleft().get()
self.print_statusline(complete, total, 1)
# Wait for the rest of the results
while len(results) > 0:
complete += results.popleft().get()
self.print_statusline(complete, total, 1)
for world in self.worlds:
try:
while (1):
# an exception will break us out of this loop
item = world.poi_q.get(block=False)
if item[0] == "newpoi":
if item[1] not in world.POI:
#print "got an item from the queue!"
world.POI.append(item[1])
elif item[0] == "removePOI":
world.persistentData['POI'] = filter(lambda x: x['chunk'] != item[1], world.persistentData['POI'])
except Queue.Empty:
pass
self.print_statusline(complete, total, 1, True)
# Now do the other layers
for zoom in xrange(self.max_p-1, 0, -1):
level = self.max_p - zoom + 1
assert len(results) == 0
complete = 0
total = 0
for q in quadtrees:
if zoom <= q.p:
total += 4**zoom
logging.info("Starting level {0}".format(level))
timestamp = time.time()
for result in self._apply_render_inntertile(pool, zoom,batch_size):
results.append(result)
# every second drain some of the queue
timestamp2 = time.time()
if timestamp2 >= timestamp + 1:
timestamp = timestamp2
count_to_remove = (1000//batch_size)
if count_to_remove < len(results):
while count_to_remove > 0:
count_to_remove -= 1
complete += results.popleft().get()
self.print_statusline(complete, total, level)
if len(results) > (10000/batch_size):
while len(results) > (500/batch_size):
complete += results.popleft().get()
self.print_statusline(complete, total, level)
# Empty the queue
while len(results) > 0:
complete += results.popleft().get()
self.print_statusline(complete, total, level)
self.print_statusline(complete, total, level, True)
logging.info("Done")
pool.close()
pool.join()
# Do the final one right here:
for q in quadtrees:
q.render_innertile(os.path.join(q.destdir, q.tiledir), "base")
def _apply_render_worldtiles(self, pool,batch_size):
"""Returns an iterator over result objects. Each time a new result is
requested, a new task is added to the pool and a result returned.
"""
if batch_size < len(self.quadtrees):
batch_size = len(self.quadtrees)
batch = []
jobcount = 0
# roundrobin add tiles to a batch job (thus they should all roughly work on similar chunks)
iterables = [q.get_worldtiles() for q in self.quadtrees]
for job in roundrobin(iterables):
# fixup so the worker knows which quadtree this is
job[0] = job[0]._render_index
# Put this in the batch to be submited to the pool
batch.append(job)
jobcount += 1
if jobcount >= batch_size:
jobcount = 0
yield pool.apply_async(func=render_worldtile_batch, args= [batch])
batch = []
if jobcount > 0:
yield pool.apply_async(func=render_worldtile_batch, args= [batch])
def _apply_render_inntertile(self, pool, zoom,batch_size):
"""Same as _apply_render_worltiles but for the inntertile routine.
Returns an iterator that yields result objects from tasks that have
been applied to the pool.
"""
if batch_size < len(self.quadtrees):
batch_size = len(self.quadtrees)
batch = []
jobcount = 0
# roundrobin add tiles to a batch job (thus they should all roughly work on similar chunks)
iterables = [q.get_innertiles(zoom) for q in self.quadtrees if zoom <= q.p]
for job in roundrobin(iterables):
# fixup so the worker knows which quadtree this is
job[0] = job[0]._render_index
# Put this in the batch to be submited to the pool
batch.append(job)
jobcount += 1
if jobcount >= batch_size:
jobcount = 0
yield pool.apply_async(func=render_innertile_batch, args= [batch])
batch = []
if jobcount > 0:
yield pool.apply_async(func=render_innertile_batch, args= [batch])
@catch_keyboardinterrupt
def render_worldtile_batch(batch):
global child_rendernode
rendernode = child_rendernode
count = 0
#logging.debug("{0} working on batch of size {1}".format(os.getpid(),len(batch)))
for job in batch:
count += 1
quadtree = rendernode.quadtrees[job[0]]
colstart = job[1]
colend = job[2]
rowstart = job[3]
rowend = job[4]
path = job[5]
poi_queue = quadtree.world.poi_q
path = quadtree.full_tiledir+os.sep+path
# (even if tilechunks is empty, render_worldtile will delete
# existing images if appropriate)
# And uses these chunks
tilechunks = quadtree.get_chunks_in_range(colstart, colend, rowstart,rowend)
#logging.debug(" tilechunks: %r", tilechunks)
quadtree.render_worldtile(tilechunks,colstart, colend, rowstart, rowend, path, poi_queue)
return count
@catch_keyboardinterrupt
def render_innertile_batch(batch):
global child_rendernode
rendernode = child_rendernode
count = 0
#logging.debug("{0} working on batch of size {1}".format(os.getpid(),len(batch)))
for job in batch:
count += 1
quadtree = rendernode.quadtrees[job[0]]
dest = quadtree.full_tiledir+os.sep+job[1]
quadtree.render_innertile(dest=dest,name=job[2])
return count
class FakeResult(object):
def __init__(self, res):
self.res = res
def get(self):
return self.res
class FakePool(object):
"""A fake pool used to render things in sync. Implements a subset of
multiprocessing.Pool"""
def apply_async(self, func, args=(), kwargs=None):
if not kwargs:
kwargs = {}
result = func(*args, **kwargs)
return FakeResult(result)
def close(self):
pass
def join(self):
pass

902
overviewer_core/src/Draw.c Normal file
View File

@@ -0,0 +1,902 @@
/*
* The Python Imaging Library.
* $Id$
*
* a simple drawing package for the Imaging library
*
* history:
* 1996-04-13 fl Created.
* 1996-04-30 fl Added transforms and polygon support.
* 1996-08-12 fl Added filled polygons.
* 1996-11-05 fl Fixed float/int confusion in polygon filler
* 1997-07-04 fl Support 32-bit images (C++ would have been nice)
* 1998-09-09 fl Eliminated qsort casts; improved rectangle clipping
* 1998-09-10 fl Fixed fill rectangle to include lower edge (!)
* 1998-12-29 fl Added arc, chord, and pieslice primitives
* 1999-01-10 fl Added some level 2 ("arrow") stuff (experimental)
* 1999-02-06 fl Added bitmap primitive
* 1999-07-26 fl Eliminated a compiler warning
* 1999-07-31 fl Pass ink as void* instead of int
* 2002-12-10 fl Added experimental RGBA-on-RGB drawing
* 2004-09-04 fl Support simple wide lines (no joins)
* 2005-05-25 fl Fixed line width calculation
* 2011-04-01 Modified for use in Minecraft-Overviewer
*
* Copyright (c) 1996-2006 by Fredrik Lundh
* Copyright (c) 1997-2006 by Secret Labs AB.
*
* This file is part of the Python Imaging Library
*
* By obtaining, using, and/or copying this software and/or its associated
* documentation, you agree that you have read, understood, and will comply
* with the following terms and conditions:
*
* Permission to use, copy, modify, and distribute this software and its
* associated documentation for any purpose and without fee is hereby granted,
* provided that the above copyright notice appears in all copies, and that
* both that copyright notice and this permission notice appear in supporting
* documentation, and that the name of Secret Labs AB or the author not be used
* in advertising or publicity pertaining to distribution of the software
* without specific, written prior permission.
*
* SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS
* SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS.
* IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR BE LIABLE FOR ANY SPECIAL,
* INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
* LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
* OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
* PERFORMANCE OF THIS SOFTWARE.
*
*/
/* FIXME: support fill/outline attribute for all filled shapes */
/* FIXME: support zero-winding fill */
/* FIXME: add drawing context, support affine transforms */
/* FIXME: support clip window (and mask?) */
#include "Imaging.h"
#include <math.h>
#define CEIL(v) (int) ceil(v)
#define FLOOR(v) ((v) >= 0.0 ? (int) (v) : (int) floor(v))
#define INK8(ink) (*(UINT8*)ink)
#define INK32(ink) (*(INT32*)ink)
/* like (a * b + 127) / 255), but much faster on most platforms */
#define MULDIV255(a, b, tmp)\
(tmp = (a) * (b) + 128, ((((tmp) >> 8) + (tmp)) >> 8))
#define BLEND(mask, in1, in2, tmp1, tmp2)\
(MULDIV255(in1, 255 - mask, tmp1) + MULDIV255(in2, mask, tmp2))
/* -------------------------------------------------------------------- */
/* Primitives */
/* -------------------------------------------------------------------- */
typedef struct {
/* edge descriptor for polygon engine */
int d;
int x0, y0;
int xmin, ymin, xmax, ymax;
float dx;
} Edge;
static inline void
point8(Imaging im, int x, int y, int ink)
{
if (x >= 0 && x < im->xsize && y >= 0 && y < im->ysize)
im->image8[y][x] = (UINT8) ink;
}
static inline void
point32(Imaging im, int x, int y, int ink)
{
if (x >= 0 && x < im->xsize && y >= 0 && y < im->ysize)
im->image32[y][x] = ink;
}
static inline void
point32rgba(Imaging im, int x, int y, int ink)
{
unsigned int tmp1, tmp2;
if (x >= 0 && x < im->xsize && y >= 0 && y < im->ysize) {
UINT8* out = (UINT8*) im->image[y]+x*4;
UINT8* in = (UINT8*) &ink;
out[0] = BLEND(in[3], out[0], in[0], tmp1, tmp2);
out[1] = BLEND(in[3], out[1], in[1], tmp1, tmp2);
out[2] = BLEND(in[3], out[2], in[2], tmp1, tmp2);
}
}
static inline void
hline8(Imaging im, int x0, int y0, int x1, int ink)
{
int tmp;
if (y0 >= 0 && y0 < im->ysize) {
if (x0 > x1)
tmp = x0, x0 = x1, x1 = tmp;
if (x0 < 0)
x0 = 0;
else if (x0 >= im->xsize)
return;
if (x1 < 0)
return;
else if (x1 >= im->xsize)
x1 = im->xsize-1;
if (x0 <= x1)
memset(im->image8[y0] + x0, (UINT8) ink, x1 - x0 + 1);
}
}
static inline void
hline32(Imaging im, int x0, int y0, int x1, int ink)
{
int tmp;
INT32* p;
if (y0 >= 0 && y0 < im->ysize) {
if (x0 > x1)
tmp = x0, x0 = x1, x1 = tmp;
if (x0 < 0)
x0 = 0;
else if (x0 >= im->xsize)
return;
if (x1 < 0)
return;
else if (x1 >= im->xsize)
x1 = im->xsize-1;
p = im->image32[y0];
while (x0 <= x1)
p[x0++] = ink;
}
}
static inline void
hline32rgba(Imaging im, int x0, int y0, int x1, int ink)
{
int tmp;
unsigned int tmp1, tmp2;
if (y0 >= 0 && y0 < im->ysize) {
if (x0 > x1)
tmp = x0, x0 = x1, x1 = tmp;
if (x0 < 0)
x0 = 0;
else if (x0 >= im->xsize)
return;
if (x1 < 0)
return;
else if (x1 >= im->xsize)
x1 = im->xsize-1;
if (x0 <= x1) {
UINT8* out = (UINT8*) im->image[y0]+x0*4;
UINT8* in = (UINT8*) &ink;
while (x0 <= x1) {
out[0] = BLEND(in[3], out[0], in[0], tmp1, tmp2);
out[1] = BLEND(in[3], out[1], in[1], tmp1, tmp2);
out[2] = BLEND(in[3], out[2], in[2], tmp1, tmp2);
x0++; out += 4;
}
}
}
}
static inline void
line8(Imaging im, int x0, int y0, int x1, int y1, int ink)
{
int i, n, e;
int dx, dy;
int xs, ys;
/* normalize coordinates */
dx = x1-x0;
if (dx < 0)
dx = -dx, xs = -1;
else
xs = 1;
dy = y1-y0;
if (dy < 0)
dy = -dy, ys = -1;
else
ys = 1;
n = (dx > dy) ? dx : dy;
if (dx == 0)
/* vertical */
for (i = 0; i < dy; i++) {
point8(im, x0, y0, ink);
y0 += ys;
}
else if (dy == 0)
/* horizontal */
for (i = 0; i < dx; i++) {
point8(im, x0, y0, ink);
x0 += xs;
}
else if (dx > dy) {
/* bresenham, horizontal slope */
n = dx;
dy += dy;
e = dy - dx;
dx += dx;
for (i = 0; i < n; i++) {
point8(im, x0, y0, ink);
if (e >= 0) {
y0 += ys;
e -= dx;
}
e += dy;
x0 += xs;
}
} else {
/* bresenham, vertical slope */
n = dy;
dx += dx;
e = dx - dy;
dy += dy;
for (i = 0; i < n; i++) {
point8(im, x0, y0, ink);
if (e >= 0) {
x0 += xs;
e -= dy;
}
e += dx;
y0 += ys;
}
}
}
static inline void
line32(Imaging im, int x0, int y0, int x1, int y1, int ink)
{
int i, n, e;
int dx, dy;
int xs, ys;
/* normalize coordinates */
dx = x1-x0;
if (dx < 0)
dx = -dx, xs = -1;
else
xs = 1;
dy = y1-y0;
if (dy < 0)
dy = -dy, ys = -1;
else
ys = 1;
n = (dx > dy) ? dx : dy;
if (dx == 0)
/* vertical */
for (i = 0; i < dy; i++) {
point32(im, x0, y0, ink);
y0 += ys;
}
else if (dy == 0)
/* horizontal */
for (i = 0; i < dx; i++) {
point32(im, x0, y0, ink);
x0 += xs;
}
else if (dx > dy) {
/* bresenham, horizontal slope */
n = dx;
dy += dy;
e = dy - dx;
dx += dx;
for (i = 0; i < n; i++) {
point32(im, x0, y0, ink);
if (e >= 0) {
y0 += ys;
e -= dx;
}
e += dy;
x0 += xs;
}
} else {
/* bresenham, vertical slope */
n = dy;
dx += dx;
e = dx - dy;
dy += dy;
for (i = 0; i < n; i++) {
point32(im, x0, y0, ink);
if (e >= 0) {
x0 += xs;
e -= dy;
}
e += dx;
y0 += ys;
}
}
}
static inline void
line32rgba(Imaging im, int x0, int y0, int x1, int y1, int ink)
{
int i, n, e;
int dx, dy;
int xs, ys;
/* normalize coordinates */
dx = x1-x0;
if (dx < 0)
dx = -dx, xs = -1;
else
xs = 1;
dy = y1-y0;
if (dy < 0)
dy = -dy, ys = -1;
else
ys = 1;
n = (dx > dy) ? dx : dy;
if (dx == 0)
/* vertical */
for (i = 0; i < dy; i++) {
point32rgba(im, x0, y0, ink);
y0 += ys;
}
else if (dy == 0)
/* horizontal */
for (i = 0; i < dx; i++) {
point32rgba(im, x0, y0, ink);
x0 += xs;
}
else if (dx > dy) {
/* bresenham, horizontal slope */
n = dx;
dy += dy;
e = dy - dx;
dx += dx;
for (i = 0; i < n; i++) {
point32rgba(im, x0, y0, ink);
if (e >= 0) {
y0 += ys;
e -= dx;
}
e += dy;
x0 += xs;
}
} else {
/* bresenham, vertical slope */
n = dy;
dx += dx;
e = dx - dy;
dy += dy;
for (i = 0; i < n; i++) {
point32rgba(im, x0, y0, ink);
if (e >= 0) {
x0 += xs;
e -= dy;
}
e += dx;
y0 += ys;
}
}
}
static int
x_cmp(const void *x0, const void *x1)
{
float diff = *((float*)x0) - *((float*)x1);
if (diff < 0)
return -1;
else if (diff > 0)
return 1;
else
return 0;
}
static inline int
polygon8(Imaging im, int n, Edge *e, int ink, int eofill)
{
int i, j;
float *xx;
int ymin, ymax;
float y;
if (n <= 0)
return 0;
/* Find upper and lower polygon boundary (within image) */
ymin = e[0].ymin;
ymax = e[0].ymax;
for (i = 1; i < n; i++) {
if (e[i].ymin < ymin) ymin = e[i].ymin;
if (e[i].ymax > ymax) ymax = e[i].ymax;
}
if (ymin < 0)
ymin = 0;
if (ymax >= im->ysize)
ymax = im->ysize-1;
/* Process polygon edges */
xx = malloc(n * sizeof(float));
if (!xx)
return -1;
for (;ymin <= ymax; ymin++) {
y = ymin+0.5F;
for (i = j = 0; i < n; i++)
if (y >= e[i].ymin && y <= e[i].ymax) {
if (e[i].d == 0)
hline8(im, e[i].xmin, ymin, e[i].xmax, ink);
else
xx[j++] = (y-e[i].y0) * e[i].dx + e[i].x0;
}
if (j == 2) {
if (xx[0] < xx[1])
hline8(im, CEIL(xx[0]-0.5), ymin, FLOOR(xx[1]+0.5), ink);
else
hline8(im, CEIL(xx[1]-0.5), ymin, FLOOR(xx[0]+0.5), ink);
} else {
qsort(xx, j, sizeof(float), x_cmp);
for (i = 0; i < j-1 ; i += 2)
hline8(im, CEIL(xx[i]-0.5), ymin, FLOOR(xx[i+1]+0.5), ink);
}
}
free(xx);
return 0;
}
static inline int
polygon32(Imaging im, int n, Edge *e, int ink, int eofill)
{
int i, j;
float *xx;
int ymin, ymax;
float y;
if (n <= 0)
return 0;
/* Find upper and lower polygon boundary (within image) */
ymin = e[0].ymin;
ymax = e[0].ymax;
for (i = 1; i < n; i++) {
if (e[i].ymin < ymin) ymin = e[i].ymin;
if (e[i].ymax > ymax) ymax = e[i].ymax;
}
if (ymin < 0)
ymin = 0;
if (ymax >= im->ysize)
ymax = im->ysize-1;
/* Process polygon edges */
xx = malloc(n * sizeof(float));
if (!xx)
return -1;
for (;ymin <= ymax; ymin++) {
y = ymin+0.5F;
for (i = j = 0; i < n; i++) {
if (y >= e[i].ymin && y <= e[i].ymax) {
if (e[i].d == 0)
hline32(im, e[i].xmin, ymin, e[i].xmax, ink);
else
xx[j++] = (y-e[i].y0) * e[i].dx + e[i].x0;
}
}
if (j == 2) {
if (xx[0] < xx[1])
hline32(im, CEIL(xx[0]-0.5), ymin, FLOOR(xx[1]+0.5), ink);
else
hline32(im, CEIL(xx[1]-0.5), ymin, FLOOR(xx[0]+0.5), ink);
} else {
qsort(xx, j, sizeof(float), x_cmp);
for (i = 0; i < j-1 ; i += 2)
hline32(im, CEIL(xx[i]-0.5), ymin, FLOOR(xx[i+1]+0.5), ink);
}
}
free(xx);
return 0;
}
static inline int
polygon32rgba(Imaging im, int n, Edge *e, int ink, int eofill)
{
int i, j;
float *xx;
int ymin, ymax;
float y;
if (n <= 0)
return 0;
/* Find upper and lower polygon boundary (within image) */
ymin = e[0].ymin;
ymax = e[0].ymax;
for (i = 1; i < n; i++) {
if (e[i].ymin < ymin) ymin = e[i].ymin;
if (e[i].ymax > ymax) ymax = e[i].ymax;
}
if (ymin < 0)
ymin = 0;
if (ymax >= im->ysize)
ymax = im->ysize-1;
/* Process polygon edges */
xx = malloc(n * sizeof(float));
if (!xx)
return -1;
for (;ymin <= ymax; ymin++) {
y = ymin+0.5F;
for (i = j = 0; i < n; i++) {
if (y >= e[i].ymin && y <= e[i].ymax) {
if (e[i].d == 0)
hline32rgba(im, e[i].xmin, ymin, e[i].xmax, ink);
else
xx[j++] = (y-e[i].y0) * e[i].dx + e[i].x0;
}
}
if (j == 2) {
if (xx[0] < xx[1])
hline32rgba(im, CEIL(xx[0]-0.5), ymin, FLOOR(xx[1]+0.5), ink);
else
hline32rgba(im, CEIL(xx[1]-0.5), ymin, FLOOR(xx[0]+0.5), ink);
} else {
qsort(xx, j, sizeof(float), x_cmp);
for (i = 0; i < j-1 ; i += 2)
hline32rgba(im, CEIL(xx[i]-0.5), ymin, FLOOR(xx[i+1]+0.5), ink);
}
}
free(xx);
return 0;
}
static inline void
add_edge(Edge *e, int x0, int y0, int x1, int y1)
{
/* printf("edge %d %d %d %d\n", x0, y0, x1, y1); */
if (x0 <= x1)
e->xmin = x0, e->xmax = x1;
else
e->xmin = x1, e->xmax = x0;
if (y0 <= y1)
e->ymin = y0, e->ymax = y1;
else
e->ymin = y1, e->ymax = y0;
if (y0 == y1) {
e->d = 0;
e->dx = 0.0;
} else {
e->dx = ((float)(x1-x0)) / (y1-y0);
if (y0 == e->ymin)
e->d = 1;
else
e->d = -1;
}
e->x0 = x0;
e->y0 = y0;
}
typedef struct {
void (*point)(Imaging im, int x, int y, int ink);
void (*hline)(Imaging im, int x0, int y0, int x1, int ink);
void (*line)(Imaging im, int x0, int y0, int x1, int y1, int ink);
int (*polygon)(Imaging im, int n, Edge *e, int ink, int eofill);
} DRAW;
DRAW draw8 = { point8, hline8, line8, polygon8 };
DRAW draw32 = { point32, hline32, line32, polygon32 };
DRAW draw32rgba = { point32rgba, hline32rgba, line32rgba, polygon32rgba };
/* -------------------------------------------------------------------- */
/* Interface */
/* -------------------------------------------------------------------- */
#define DRAWINIT()\
if (im->image8) {\
draw = &draw8;\
ink = INK8(ink_);\
} else {\
draw = (op) ? &draw32rgba : &draw32; \
ink = INK32(ink_);\
}
int
ImagingDrawPoint(Imaging im, int x0, int y0, const void* ink_, int op)
{
DRAW* draw;
INT32 ink;
DRAWINIT();
draw->point(im, x0, y0, ink);
return 0;
}
int
ImagingDrawLine(Imaging im, int x0, int y0, int x1, int y1, const void* ink_,
int op)
{
DRAW* draw;
INT32 ink;
DRAWINIT();
draw->line(im, x0, y0, x1, y1, ink);
return 0;
}
int
ImagingDrawWideLine(Imaging im, int x0, int y0, int x1, int y1,
const void* ink_, int width, int op)
{
DRAW* draw;
INT32 ink;
Edge e[4];
int dx, dy;
double d;
DRAWINIT();
if (width <= 1) {
draw->line(im, x0, y0, x1, y1, ink);
return 0;
}
dx = x1-x0;
dy = y1-y0;
if (dx == 0 && dy == 0) {
draw->point(im, x0, y0, ink);
return 0;
}
d = width / sqrt((float) (dx*dx + dy*dy)) / 2.0;
dx = (int) floor(d * (y1-y0) + 0.5);
dy = (int) floor(d * (x1-x0) + 0.5);
add_edge(e+0, x0 - dx, y0 + dy, x1 - dx, y1 + dy);
add_edge(e+1, x1 - dx, y1 + dy, x1 + dx, y1 - dy);
add_edge(e+2, x1 + dx, y1 - dy, x0 + dx, y0 - dy);
add_edge(e+3, x0 + dx, y0 - dy, x0 - dx, y0 + dy);
draw->polygon(im, 4, e, ink, 0);
return 0;
}
/* -------------------------------------------------------------------- */
/* standard shapes */
#define ARC 0
#define CHORD 1
#define PIESLICE 2
/* -------------------------------------------------------------------- */
/* experimental level 2 ("arrow") graphics stuff. this implements
portions of the arrow api on top of the Edge structure. the
semantics are ok, except that "curve" flattens the bezier curves by
itself */
#if 1 /* ARROW_GRAPHICS */
struct ImagingOutlineInstance {
float x0, y0;
float x, y;
int count;
Edge *edges;
int size;
};
void
ImagingOutlineDelete(ImagingOutline outline)
{
if (!outline)
return;
if (outline->edges)
free(outline->edges);
free(outline);
}
static Edge*
allocate(ImagingOutline outline, int extra)
{
Edge* e;
if (outline->count + extra > outline->size) {
/* expand outline buffer */
outline->size += extra + 25;
if (!outline->edges)
e = malloc(outline->size * sizeof(Edge));
else
e = realloc(outline->edges, outline->size * sizeof(Edge));
if (!e)
return NULL;
outline->edges = e;
}
e = outline->edges + outline->count;
outline->count += extra;
return e;
}
int
ImagingOutlineMove(ImagingOutline outline, float x0, float y0)
{
outline->x = outline->x0 = x0;
outline->y = outline->y0 = y0;
return 0;
}
int
ImagingOutlineLine(ImagingOutline outline, float x1, float y1)
{
Edge* e;
e = allocate(outline, 1);
if (!e)
return -1; /* out of memory */
add_edge(e, (int) outline->x, (int) outline->y, (int) x1, (int) y1);
outline->x = x1;
outline->y = y1;
return 0;
}
int
ImagingOutlineCurve(ImagingOutline outline, float x1, float y1,
float x2, float y2, float x3, float y3)
{
Edge* e;
int i;
float xo, yo;
#define STEPS 32
e = allocate(outline, STEPS);
if (!e)
return -1; /* out of memory */
xo = outline->x;
yo = outline->y;
/* flatten the bezier segment */
for (i = 1; i <= STEPS; i++) {
float t = ((float) i) / STEPS;
float t2 = t*t;
float t3 = t2*t;
float u = 1.0F - t;
float u2 = u*u;
float u3 = u2*u;
float x = outline->x*u3 + 3*(x1*t*u2 + x2*t2*u) + x3*t3 + 0.5;
float y = outline->y*u3 + 3*(y1*t*u2 + y2*t2*u) + y3*t3 + 0.5;
add_edge(e++, xo, yo, (int) x, (int) y);
xo = x, yo = y;
}
outline->x = xo;
outline->y = yo;
return 0;
}
int
ImagingOutlineCurve2(ImagingOutline outline, float cx, float cy,
float x3, float y3)
{
/* add bezier curve based on three control points (as
in the Flash file format) */
return ImagingOutlineCurve(
outline,
(outline->x + cx + cx)/3, (outline->y + cy + cy)/3,
(cx + cx + x3)/3, (cy + cy + y3)/3,
x3, y3);
}
int
ImagingOutlineClose(ImagingOutline outline)
{
if (outline->x == outline->x0 && outline->y == outline->y0)
return 0;
return ImagingOutlineLine(outline, outline->x0, outline->y0);
}
int
ImagingDrawOutline(Imaging im, ImagingOutline outline, const void* ink_,
int fill, int op)
{
DRAW* draw;
INT32 ink;
DRAWINIT();
draw->polygon(im, outline->count, outline->edges, ink, 0);
return 0;
}
#endif

View File

@@ -0,0 +1,360 @@
/*
* 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/>.
*/
/*
* This file implements a custom alpha_over function for (some) PIL
* images. It's designed to be used through composite.py, which
* includes a proxy alpha_over function that falls back to the default
* PIL paste if this extension is not found.
*/
#include "overviewer.h"
/* like (a * b + 127) / 255), but much faster on most platforms
from PIL's _imaging.c */
#define MULDIV255(a, b, tmp) \
(tmp = (a) * (b) + 128, ((((tmp) >> 8) + (tmp)) >> 8))
typedef struct {
PyObject_HEAD
Imaging image;
} ImagingObject;
inline Imaging
imaging_python_to_c(PyObject *obj)
{
PyObject *im;
Imaging image;
/* first, get the 'im' attribute */
im = PyObject_GetAttrString(obj, "im");
if (!im)
return NULL;
/* make sure 'im' is the right type */
if (strcmp(im->ob_type->tp_name, "ImagingCore") != 0) {
/* it's not -- raise an error and exit */
PyErr_SetString(PyExc_TypeError,
"image attribute 'im' is not a core Imaging type");
return NULL;
}
image = ((ImagingObject *)im)->image;
Py_DECREF(im);
return image;
}
/* helper function to setup s{x,y}, d{x,y}, and {x,y}size variables
in these composite functions -- even handles auto-sizing to src! */
static inline void
setup_source_destination(Imaging src, Imaging dest,
int *sx, int *sy, int *dx, int *dy, int *xsize, int *ysize)
{
/* handle negative/zero sizes appropriately */
if (*xsize <= 0 || *ysize <= 0) {
*xsize = src->xsize;
*ysize = src->ysize;
}
/* set up the source position, size and destination position */
/* handle negative dest pos */
if (*dx < 0) {
*sx = -(*dx);
*dx = 0;
} else {
*sx = 0;
}
if (*dy < 0) {
*sy = -(*dy);
*dy = 0;
} else {
*sy = 0;
}
/* set up source dimensions */
*xsize -= *sx;
*ysize -= *sy;
/* clip dimensions, if needed */
if (*dx + *xsize > dest->xsize)
*xsize = dest->xsize - *dx;
if (*dy + *ysize > dest->ysize)
*ysize = dest->ysize - *dy;
}
/* convenience alpha_over with 1.0 as overall_alpha */
inline PyObject* alpha_over(PyObject *dest, PyObject *src, PyObject *mask,
int dx, int dy, int xsize, int ysize) {
return alpha_over_full(dest, src, mask, 1.0f, dx, dy, xsize, ysize);
}
/* the full alpha_over function, in a form that can be called from C
* overall_alpha is multiplied with the whole mask, useful for lighting...
* if xsize, ysize are negative, they are instead set to the size of the image in src
* returns NULL on error, dest on success. You do NOT need to decref the return!
*/
inline PyObject *
alpha_over_full(PyObject *dest, PyObject *src, PyObject *mask, float overall_alpha,
int dx, int dy, int xsize, int ysize) {
/* libImaging handles */
Imaging imDest, imSrc, imMask;
/* cached blend properties */
int src_has_alpha, mask_offset, mask_stride;
/* source position */
int sx, sy;
/* iteration variables */
unsigned int x, y, i;
/* temporary calculation variables */
int tmp1, tmp2, tmp3;
/* integer [0, 255] version of overall_alpha */
UINT8 overall_alpha_int = 255 * overall_alpha;
/* short-circuit this whole thing if overall_alpha is zero */
if (overall_alpha_int == 0)
return dest;
imDest = imaging_python_to_c(dest);
imSrc = imaging_python_to_c(src);
imMask = imaging_python_to_c(mask);
if (!imDest || !imSrc || !imMask)
return NULL;
/* check the various image modes, make sure they make sense */
if (strcmp(imDest->mode, "RGBA") != 0) {
PyErr_SetString(PyExc_ValueError,
"given destination image does not have mode \"RGBA\"");
return NULL;
}
if (strcmp(imSrc->mode, "RGBA") != 0 && strcmp(imSrc->mode, "RGB") != 0) {
PyErr_SetString(PyExc_ValueError,
"given source image does not have mode \"RGBA\" or \"RGB\"");
return NULL;
}
if (strcmp(imMask->mode, "RGBA") != 0 && strcmp(imMask->mode, "L") != 0) {
PyErr_SetString(PyExc_ValueError,
"given mask image does not have mode \"RGBA\" or \"L\"");
return NULL;
}
/* make sure mask size matches src size */
if (imSrc->xsize != imMask->xsize || imSrc->ysize != imMask->ysize) {
PyErr_SetString(PyExc_ValueError,
"mask and source image sizes do not match");
return NULL;
}
/* set up flags for the src/mask type */
src_has_alpha = (imSrc->pixelsize == 4 ? 1 : 0);
/* how far into image the first alpha byte resides */
mask_offset = (imMask->pixelsize == 4 ? 3 : 0);
/* how many bytes to skip to get to the next alpha byte */
mask_stride = imMask->pixelsize;
/* setup source & destination vars */
setup_source_destination(imSrc, imDest, &sx, &sy, &dx, &dy, &xsize, &ysize);
/* check that there remains any blending to be done */
if (xsize <= 0 || ysize <= 0) {
/* nothing to do, return */
return dest;
}
for (y = 0; y < ysize; y++) {
UINT8 *out = (UINT8 *)imDest->image[dy + y] + dx * 4;
UINT8 *outmask = (UINT8 *)imDest->image[dy + y] + dx * 4 + 3;
UINT8 *in = (UINT8 *)imSrc->image[sy + y] + sx * (imSrc->pixelsize);
UINT8 *inmask = (UINT8 *)imMask->image[sy + y] + sx * mask_stride + mask_offset;
for (x = 0; x < xsize; x++) {
UINT8 in_alpha;
/* apply overall_alpha */
if (overall_alpha_int != 255 && *inmask != 0) {
in_alpha = MULDIV255(*inmask, overall_alpha_int, tmp1);
} else {
in_alpha = *inmask;
}
/* special cases */
if (in_alpha == 255 || *outmask == 0) {
*outmask = in_alpha;
*out = *in;
out++, in++;
*out = *in;
out++, in++;
*out = *in;
out++, in++;
} else if (in_alpha == 0) {
/* do nothing -- source is fully transparent */
out += 3;
in += 3;
} else {
/* general case */
int alpha = in_alpha + MULDIV255(*outmask, 255 - in_alpha, tmp1);
for (i = 0; i < 3; i++) {
/* general case */
*out = MULDIV255(*in, in_alpha, tmp1) +
MULDIV255(MULDIV255(*out, *outmask, tmp2), 255 - in_alpha, tmp3);
*out = (*out * 255) / alpha;
out++, in++;
}
*outmask = alpha;
}
out++;
if (src_has_alpha)
in++;
outmask += 4;
inmask += mask_stride;
}
}
return dest;
}
/* wraps alpha_over so it can be called directly from python */
/* properly refs the return value when needed: you DO need to decref the return */
PyObject *
alpha_over_wrap(PyObject *self, PyObject *args)
{
/* raw input python variables */
PyObject *dest, *src, *pos, *mask;
/* destination position and size */
int dx, dy, xsize, ysize;
/* return value: dest image on success */
PyObject *ret;
if (!PyArg_ParseTuple(args, "OOOO", &dest, &src, &pos, &mask))
return NULL;
/* destination position read */
if (!PyArg_ParseTuple(pos, "iiii", &dx, &dy, &xsize, &ysize)) {
/* try again, but this time try to read a point */
PyErr_Clear();
xsize = 0;
ysize = 0;
if (!PyArg_ParseTuple(pos, "ii", &dx, &dy)) {
PyErr_SetString(PyExc_TypeError,
"given blend destination rect is not valid");
return NULL;
}
}
ret = alpha_over(dest, src, mask, dx, dy, xsize, ysize);
if (ret == dest) {
/* Python needs us to own our return value */
Py_INCREF(dest);
}
return ret;
}
/* like alpha_over, but instead of src image it takes a source color
* also, it multiplies instead of doing an over operation
*/
PyObject *
tint_with_mask(PyObject *dest, unsigned char sr, unsigned char sg,
unsigned char sb, unsigned char sa,
PyObject *mask, int dx, int dy, int xsize, int ysize) {
/* libImaging handles */
Imaging imDest, imMask;
/* cached blend properties */
int mask_offset, mask_stride;
/* source position */
int sx, sy;
/* iteration variables */
unsigned int x, y;
/* temporary calculation variables */
int tmp1, tmp2;
imDest = imaging_python_to_c(dest);
imMask = imaging_python_to_c(mask);
if (!imDest || !imMask)
return NULL;
/* check the various image modes, make sure they make sense */
if (strcmp(imDest->mode, "RGBA") != 0) {
PyErr_SetString(PyExc_ValueError,
"given destination image does not have mode \"RGBA\"");
return NULL;
}
if (strcmp(imMask->mode, "RGBA") != 0 && strcmp(imMask->mode, "L") != 0) {
PyErr_SetString(PyExc_ValueError,
"given mask image does not have mode \"RGBA\" or \"L\"");
return NULL;
}
/* how far into image the first alpha byte resides */
mask_offset = (imMask->pixelsize == 4 ? 3 : 0);
/* how many bytes to skip to get to the next alpha byte */
mask_stride = imMask->pixelsize;
/* setup source & destination vars */
setup_source_destination(imMask, imDest, &sx, &sy, &dx, &dy, &xsize, &ysize);
/* check that there remains any blending to be done */
if (xsize <= 0 || ysize <= 0) {
/* nothing to do, return */
return dest;
}
for (y = 0; y < ysize; y++) {
UINT8 *out = (UINT8 *)imDest->image[dy + y] + dx * 4;
UINT8 *inmask = (UINT8 *)imMask->image[sy + y] + sx * mask_stride + mask_offset;
for (x = 0; x < xsize; x++) {
/* special cases */
if (*inmask == 255) {
*out = MULDIV255(*out, sr, tmp1);
out++;
*out = MULDIV255(*out, sg, tmp1);
out++;
*out = MULDIV255(*out, sb, tmp1);
out++;
*out = MULDIV255(*out, sa, tmp1);
out++;
} else if (*inmask == 0) {
/* do nothing -- source is fully transparent */
out += 4;
} else {
/* general case */
/* TODO work out general case */
*out = MULDIV255(*out, (255 - *inmask) + MULDIV255(sr, *inmask, tmp1), tmp2);
out++;
*out = MULDIV255(*out, (255 - *inmask) + MULDIV255(sg, *inmask, tmp1), tmp2);
out++;
*out = MULDIV255(*out, (255 - *inmask) + MULDIV255(sb, *inmask, tmp1), tmp2);
out++;
*out = MULDIV255(*out, (255 - *inmask) + MULDIV255(sa, *inmask, tmp1), tmp2);
out++;
}
inmask += mask_stride;
}
}
return dest;
}

View File

@@ -0,0 +1,39 @@
/*
* 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/>.
*/
/* simple routines for dealing with endian conversion */
#define UNKNOWN_ENDIAN 0
#define BIG_ENDIAN 1
#define LITTLE_ENDIAN 2
static int endianness = UNKNOWN_ENDIAN;
void init_endian(void) {
/* figure out what our endianness is! */
short word = 0x0001;
char* byte = (char*)(&word);
endianness = byte[0] ? LITTLE_ENDIAN : BIG_ENDIAN;
}
unsigned short big_endian_ushort(unsigned short in) {
return (endianness == LITTLE_ENDIAN) ? ((in >> 8) | (in << 8)) : in;
}
unsigned int big_endian_uint(unsigned int in) {
return (endianness == LITTLE_ENDIAN) ? (((in & 0x000000FF) << 24) + ((in & 0x0000FF00) << 8) + ((in & 0x00FF0000) >> 8) + ((in & 0xFF000000) >> 24)) : in;
}

View File

@@ -0,0 +1,453 @@
/*
* 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/>.
*/
#include "overviewer.h"
static PyObject *textures = NULL;
static PyObject *chunk_mod = NULL;
static PyObject *blockmap = NULL;
static PyObject *special_blocks = NULL;
static PyObject *specialblockmap = NULL;
static PyObject *transparent_blocks = NULL;
PyObject *init_chunk_render(PyObject *self, PyObject *args) {
/* this function only needs to be called once, anything more is an
* error... */
if (blockmap) {
PyErr_SetString(PyExc_RuntimeError, "init_chunk_render should only be called once per process.");
return NULL;
}
textures = PyImport_ImportModule("overviewer_core.textures");
/* ensure none of these pointers are NULL */
if ((!textures)) {
return NULL;
}
chunk_mod = PyImport_ImportModule("overviewer_core.chunk");
/* ensure none of these pointers are NULL */
if ((!chunk_mod)) {
return NULL;
}
blockmap = PyObject_GetAttrString(textures, "blockmap");
if (!blockmap)
return NULL;
special_blocks = PyObject_GetAttrString(textures, "special_blocks");
if (!special_blocks)
return NULL;
specialblockmap = PyObject_GetAttrString(textures, "specialblockmap");
if (!specialblockmap)
return NULL;
transparent_blocks = PyObject_GetAttrString(chunk_mod, "transparent_blocks");
if (!transparent_blocks)
return NULL;
Py_RETURN_NONE;
}
int
is_transparent(unsigned char b) {
PyObject *block = PyInt_FromLong(b);
int ret = PySequence_Contains(transparent_blocks, block);
Py_DECREF(block);
return ret;
}
unsigned char
check_adjacent_blocks(RenderState *state, int x,int y,int z, unsigned char blockid) {
/*
* Generates a pseudo ancillary data for blocks that depend of
* what are surrounded and don't have ancillary data. This
* function is through generate_pseudo_data.
*
* This uses a binary number of 4 digits to encode the info.
* The encode is:
*
* 0b1234:
* Bit: 1 2 3 4
* Side: +x +y -x -y
* Values: bit = 0 -> The corresponding side block has different blockid
* bit = 1 -> The corresponding side block has same blockid
* Example: if the bit1 is 1 that means that there is a block with
* blockid in the side of the +x direction.
*/
unsigned char pdata=0;
if (state->x == 15) { /* +x direction */
if (state->up_right_blocks != Py_None) { /* just in case we are in the end of the world */
if (getArrayByte3D(state->up_right_blocks, 0, y, z) == blockid) {
pdata = pdata|(1 << 3);
}
}
} else {
if (getArrayByte3D(state->blocks, x + 1, y, z) == blockid) {
pdata = pdata|(1 << 3);
}
}
if (state->y == 15) { /* +y direction*/
if (state->right_blocks != Py_None) {
if (getArrayByte3D(state->right_blocks, x, 0, z) == blockid) {
pdata = pdata|(1 << 2);
}
}
} else {
if (getArrayByte3D(state->blocks, x, y + 1, z) == blockid) {
pdata = pdata|(1 << 2);
}
}
if (state->x == 0) { /* -x direction*/
if (state->left_blocks != Py_None) {
if (getArrayByte3D(state->left_blocks, 15, y, z) == blockid) {
pdata = pdata|(1 << 1);
}
}
} else {
if (getArrayByte3D(state->blocks, x - 1, y, z) == blockid) {
pdata = pdata|(1 << 1);
}
}
if (state->y == 0) { /* -y direction */
if (state->up_left_blocks != Py_None) {
if (getArrayByte3D(state->up_left_blocks, x, 15, z) == blockid) {
pdata = pdata|(1 << 0);
}
}
} else {
if (getArrayByte3D(state->blocks, x, y - 1, z) == blockid) {
pdata = pdata|(1 << 0);
}
}
return pdata;
}
unsigned char
generate_pseudo_data(RenderState *state, unsigned char ancilData) {
/*
* Generates a fake ancillary data for blocks that are drawn
* depending on what are surrounded.
*/
int x = state->x, y = state->y, z = state->z;
unsigned char data = 0;
if (state->block == 2) { /* grass */
/* return 0x10 if grass is covered in snow */
if (z < 127 && getArrayByte3D(state->blocks, x, y, z+1) == 78)
return 0x10;
return ancilData;
} else if (state->block == 9) { /* water */
/* an aditional bit for top is added to the 4 bits of check_adjacent_blocks */
if (ancilData == 0) { /* static water */
if ((z != 127) && (getArrayByte3D(state->blocks, x, y, z+1) == 9)) {
data = 0;
} else {
data = 16;
}
return data; /* = 0b10000 */
} else if ((ancilData > 0) && (ancilData < 8)) { /* flowing water */
data = (check_adjacent_blocks(state, x, y, z, state->block) ^ 0x0f) | 0x10;
return data;
} else if (ancilData >= 8) { /* falling water */
data = (check_adjacent_blocks(state, x, y, z, state->block) ^ 0x0f);
return data;
}
} else if (state->block == 85) { /* fences */
return check_adjacent_blocks(state, x, y, z, state->block);
} else if (state->block == 55) { /* redstone */
/* three addiotional bit are added, one for on/off state, and
* another two for going-up redstone wire in the same block
* (connection with the level z+1) */
unsigned char above_level_data = 0, same_level_data = 0, below_level_data = 0, possibly_connected = 0, final_data = 0;
/* check for air in z+1, no air = no connection with upper level */
if ((z != 127) && (getArrayByte3D(state->left_blocks, x, y, z) == 0)) {
above_level_data = check_adjacent_blocks(state, x, y, z + 1, state->block);
} /* else above_level_data = 0 */
/* check connection with same level */
same_level_data = check_adjacent_blocks(state, x, y, z, 55);
/* check the posibility of connection with z-1 level, check for air */
possibly_connected = check_adjacent_blocks(state, x, y, z, 0);
/* check connection with z-1 level */
if (z != 0) {
below_level_data = check_adjacent_blocks(state, x, y, z - 1, state->block);
} /* else below_level_data = 0 */
final_data = above_level_data | same_level_data | (below_level_data & possibly_connected);
/* add the three bits */
if (ancilData > 0) { /* powered redstone wire */
final_data = final_data | 0x40;
}
if ((above_level_data & 0x01)) { /* draw top left going up redstonewire */
final_data = final_data | 0x20;
}
if ((above_level_data & 0x08)) { /* draw top right going up redstonewire */
final_data = final_data | 0x10;
}
return final_data;
} else if (state-> block == 54) { /* chests */
/* the top 2 bits are used to store the type of chest
* (single or double), the 2 bottom bits are used for
* orientation, look textures.py for more information. */
/* if placed alone chests always face west, return 0 to make a
* chest facing west */
unsigned char chest_data = 0, air_data = 0, final_data = 0;
/* search for chests */
chest_data = check_adjacent_blocks(state, x, y, z, 54);
/* search for air */
air_data = check_adjacent_blocks(state, x, y, z, 0);
if (chest_data == 1) { /* another chest in the east */
final_data = final_data | 0x8; /* only can face to north or south */
if ( (air_data & 0x2) == 2 ) {
final_data = final_data | 0x1; /* facing north */
} else {
final_data = final_data | 0x3; /* facing south */
}
} else if (chest_data == 2) { /* in the north */
final_data = final_data | 0x4; /* only can face to east or west */
if ( !((air_data & 0x4) == 4) ) { /* 0 = west */
final_data = final_data | 0x2; /* facing east */
}
} else if (chest_data == 4) { /*in the west */
final_data = final_data | 0x4;
if ( (air_data & 0x2) == 2 ) {
final_data = final_data | 0x1; /* facing north */
} else {
final_data = final_data | 0x3; /* facing south */
}
} else if (chest_data == 8) { /*in the south */
final_data = final_data | 0x8;
if ( !((air_data & 0x4) == 4) ) {
final_data = final_data | 0x2; /* facing east */
}
} else if (chest_data == 0) {
/* Single chest, determine the orientation */
if ( ((air_data & 0x8) == 0) && ((air_data & 0x2) == 2) ) { /* block in +x and no block in -x */
final_data = final_data | 0x1; /* facing north */
} else if ( ((air_data & 0x2) == 0) && ((air_data & 0x8) == 8)) {
final_data = final_data | 0x3;
} else if ( ((air_data & 0x4) == 0) && ((air_data & 0x1) == 1)) {
final_data = final_data | 0x2;
} /* else, facing west, value = 0 */
} else {
/* more than one adjacent chests! render as normal chest */
return 0;
}
return final_data;
} else if (state->block == 90) {
return check_adjacent_blocks(state, x, y, z, state->block);
}
return 0;
}
/* TODO triple check this to make sure reference counting is correct */
PyObject*
chunk_render(PyObject *self, PyObject *args) {
RenderState state;
int xoff, yoff;
PyObject *imgsize, *imgsize0_py, *imgsize1_py;
int imgsize0, imgsize1;
PyObject *blocks_py;
PyObject *left_blocks_py;
PyObject *right_blocks_py;
PyObject *up_left_blocks_py;
PyObject *up_right_blocks_py;
RenderModeInterface *rendermode;
void *rm_data;
PyObject *t = NULL;
if (!PyArg_ParseTuple(args, "OOiiO", &state.self, &state.img, &xoff, &yoff, &state.blockdata_expanded))
return NULL;
/* fill in important modules */
state.textures = textures;
state.chunk = chunk_mod;
/* set up the render mode */
rendermode = get_render_mode(&state);
rm_data = calloc(1, rendermode->data_size);
if (rendermode->start(rm_data, &state)) {
free(rm_data);
return Py_BuildValue("i", "-1");
}
/* get the image size */
imgsize = PyObject_GetAttrString(state.img, "size");
imgsize0_py = PySequence_GetItem(imgsize, 0);
imgsize1_py = PySequence_GetItem(imgsize, 1);
Py_DECREF(imgsize);
imgsize0 = PyInt_AsLong(imgsize0_py);
imgsize1 = PyInt_AsLong(imgsize1_py);
Py_DECREF(imgsize0_py);
Py_DECREF(imgsize1_py);
/* get the block data directly from numpy: */
blocks_py = PyObject_GetAttrString(state.self, "blocks");
state.blocks = blocks_py;
left_blocks_py = PyObject_GetAttrString(state.self, "left_blocks");
state.left_blocks = left_blocks_py;
right_blocks_py = PyObject_GetAttrString(state.self, "right_blocks");
state.right_blocks = right_blocks_py;
up_left_blocks_py = PyObject_GetAttrString(state.self, "up_left_blocks");
state.up_left_blocks = up_left_blocks_py;
up_right_blocks_py = PyObject_GetAttrString(state.self, "up_right_blocks");
state.up_right_blocks = up_right_blocks_py;
for (state.x = 15; state.x > -1; state.x--) {
for (state.y = 0; state.y < 16; state.y++) {
PyObject *blockid = NULL;
/* set up the render coordinates */
state.imgx = xoff + state.x*12 + state.y*12;
/* 128*12 -- offset for z direction, 15*6 -- offset for x */
state.imgy = yoff - state.x*6 + state.y*6 + 128*12 + 15*6;
for (state.z = 0; state.z < 128; state.z++) {
state.imgy -= 12;
/* get blockid */
state.block = getArrayByte3D(blocks_py, state.x, state.y, state.z);
if (state.block == 0) {
continue;
}
/* make sure we're rendering inside the image boundaries */
if ((state.imgx >= imgsize0 + 24) || (state.imgx <= -24)) {
continue;
}
if ((state.imgy >= imgsize1 + 24) || (state.imgy <= -24)) {
continue;
}
/* decref'd on replacement *and* at the end of the z for block */
if (blockid) {
Py_DECREF(blockid);
}
blockid = PyInt_FromLong(state.block);
// check for occlusion
if (rendermode->occluded(rm_data, &state)) {
continue;
}
// everything stored here will be a borrowed ref
/* get the texture and mask from block type / ancil. data */
if (!PySequence_Contains(special_blocks, blockid)) {
/* t = textures.blockmap[blockid] */
t = PyList_GetItem(blockmap, state.block);
} else {
PyObject *tmp;
unsigned char ancilData = getArrayByte3D(state.blockdata_expanded, state.x, state.y, state.z);
if ((state.block == 85) || (state.block == 9) || (state.block == 55) || (state.block == 54) || (state.block == 2) || (state.block == 90)) {
ancilData = generate_pseudo_data(&state, ancilData);
}
tmp = PyTuple_New(2);
Py_INCREF(blockid); /* because SetItem steals */
PyTuple_SetItem(tmp, 0, blockid);
PyTuple_SetItem(tmp, 1, PyInt_FromLong(ancilData));
/* this is a borrowed reference. no need to decref */
t = PyDict_GetItem(specialblockmap, tmp);
Py_DECREF(tmp);
}
/* if we found a proper texture, render it! */
if (t != NULL && t != Py_None)
{
PyObject *src, *mask, *mask_light;
src = PyTuple_GetItem(t, 0);
mask = PyTuple_GetItem(t, 1);
mask_light = PyTuple_GetItem(t, 2);
if (mask == Py_None)
mask = src;
rendermode->draw(rm_data, &state, src, mask, mask_light);
}
}
if (blockid) {
Py_DECREF(blockid);
blockid = NULL;
}
}
}
/* free up the rendermode info */
rendermode->finish(rm_data, &state);
free(rm_data);
Py_DECREF(blocks_py);
Py_XDECREF(left_blocks_py);
Py_XDECREF(right_blocks_py);
Py_XDECREF(up_left_blocks_py);
Py_XDECREF(up_right_blocks_py);
return Py_BuildValue("i",2);
}

View File

@@ -0,0 +1,60 @@
/*
* 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/>.
*/
#include "overviewer.h"
PyObject *get_extension_version(PyObject *self, PyObject *args) {
return Py_BuildValue("i", OVERVIEWER_EXTENSION_VERSION);
}
static PyMethodDef COverviewerMethods[] = {
{"alpha_over", alpha_over_wrap, METH_VARARGS,
"alpha over composite function"},
{"init_chunk_render", init_chunk_render, METH_VARARGS,
"Initializes the stuffs renderer."},
{"render_loop", chunk_render, METH_VARARGS,
"Renders stuffs"},
{"get_render_modes", get_render_modes, METH_VARARGS,
"returns available render modes"},
{"get_render_mode_info", get_render_mode_info, METH_VARARGS,
"returns info for a particular render mode"},
{"get_render_mode_parent", get_render_mode_parent, METH_VARARGS,
"returns parent for a particular render mode"},
{"get_render_mode_inheritance", get_render_mode_inheritance, METH_VARARGS,
"returns inheritance chain for a particular render mode"},
{"get_render_mode_children", get_render_mode_children, METH_VARARGS,
"returns (direct) children for a particular render mode"},
{"extension_version", get_extension_version, METH_VARARGS,
"Returns the extension version"},
{NULL, NULL, 0, NULL} /* Sentinel */
};
PyMODINIT_FUNC
initc_overviewer(void)
{
(void)Py_InitModule("c_overviewer", COverviewerMethods);
/* for numpy */
import_array();
init_endian();
}

View File

@@ -0,0 +1,92 @@
/*
* 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/>.
*/
/*
* This is a general include file for the Overviewer C extension. It
* lists useful, defined functions as well as those that are exported
* to python, so all files can use them.
*/
#ifndef __OVERVIEWER_H_INCLUDED__
#define __OVERVIEWER_H_INCLUDED__
// increment this value if you've made a change to the c extesion
// and want to force users to rebuild
#define OVERVIEWER_EXTENSION_VERSION 8
/* Python PIL, and numpy headers */
#include <Python.h>
#include <Imaging.h>
#include <numpy/arrayobject.h>
/* 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))))
/* generally useful MAX / MIN macros */
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define MIN(a, b) ((a) < (b) ? (a) : (b))
/* in composite.c */
Imaging imaging_python_to_c(PyObject *obj);
PyObject *alpha_over(PyObject *dest, PyObject *src, PyObject *mask,
int dx, int dy, int xsize, int ysize);
PyObject *alpha_over_full(PyObject *dest, PyObject *src, PyObject *mask, float overall_alpha,
int dx, int dy, int xsize, int ysize);
PyObject *alpha_over_wrap(PyObject *self, PyObject *args);
PyObject *tint_with_mask(PyObject *dest, unsigned char sr, unsigned char sg,
unsigned char sb, unsigned char sa,
PyObject *mask, int dx, int dy, int xsize, int ysize);
/* in iterate.c */
typedef struct {
/* the ChunkRenderer object */
PyObject *self;
/* important modules, for convenience */
PyObject *textures;
PyObject *chunk;
/* the rest only make sense for occluded() and draw() !! */
/* the tile image and destination */
PyObject *img;
int imgx, imgy;
/* the block position and type, and the block array */
int x, y, z;
unsigned char block;
PyObject *blockdata_expanded;
PyObject *blocks;
PyObject *up_left_blocks;
PyObject *up_right_blocks;
PyObject *left_blocks;
PyObject *right_blocks;
} RenderState;
PyObject *init_chunk_render(PyObject *self, PyObject *args);
int is_transparent(unsigned char b);
PyObject *chunk_render(PyObject *self, PyObject *args);
/* pull in the rendermode info */
#include "rendermodes.h"
/* in endian.c */
void init_endian(void);
unsigned short big_endian_ushort(unsigned short in);
unsigned int big_endian_uint(unsigned int in);
#endif /* __OVERVIEWER_H_INCLUDED__ */

View File

@@ -0,0 +1,234 @@
/*
* 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/>.
*/
#include "overviewer.h"
#include <math.h>
//~
//~ /* figures out the black_coeff from a given skylight and blocklight, used in
//~ lighting calculations -- note this is *different* from the one in
//~ rendermode-lighting.c (the "skylight - 11" part) */
//~ static float calculate_darkness(unsigned char skylight, unsigned char blocklight) {
//~ return 1.0f - powf(0.8f, 15.0 - MAX(blocklight, skylight - 11));
//~ }
static int
rendermode_cave_occluded(void *data, RenderState *state) {
int x = state->x, y = state->y, z = state->z, dz = 0;
RenderModeCave* self;
self = (RenderModeCave *)data;
/* check if the block is touching skylight */
if (z != 127) {
if (getArrayByte3D(self->skylight, x, y, z+1) != 0) {
return 1;
}
if ((x == 15)) {
if (self->up_right_skylight != Py_None) {
if (getArrayByte3D(self->up_right_skylight, 0, y, z) != 0) {
return 1;
}
}
} else {
if (getArrayByte3D(self->skylight, x+1, y, z) != 0) {
return 1;
}
}
if (x == 0) {
if (self->left_skylight != Py_None) {
if (getArrayByte3D(self->left_skylight, 15, y, z) != 0) {
return 1;
}
}
} else {
if (getArrayByte3D(self->skylight, x-1, y, z) != 0) {
return 1;
}
}
if (y == 15) {
if (self->right_skylight != Py_None) {
if (getArrayByte3D(self->right_skylight, 0, y, z) != 0) {
return 1;
}
}
} else {
if (getArrayByte3D(self->skylight, x, y+1, z) != 0) {
return 1;
}
}
if (y == 0) {
if (self->up_left_skylight != Py_None) {
if (getArrayByte3D(self->up_left_skylight, 15, y, z) != 0) {
return 1;
}
}
} else {
if (getArrayByte3D(self->skylight, x, y-1, z) != 0) {
return 1;
}
}
/* check for normal occlusion */
/* use ajacent chunks, if not you get blocks spreaded in chunk edges */
if ( (x == 0) && (y != 15) ) {
if (state->left_blocks != Py_None) {
if (!is_transparent(getArrayByte3D(state->left_blocks, 15, y, z)) &&
!is_transparent(getArrayByte3D(state->blocks, x, y, z+1)) &&
!is_transparent(getArrayByte3D(state->blocks, x, y+1, z))) {
return 1;
}
} else {
return 1;
}
}
if ( (x != 0) && (y == 15) ) {
if (state->right_blocks != Py_None) {
if (!is_transparent(getArrayByte3D(state->blocks, x-1, y, z)) &&
!is_transparent(getArrayByte3D(state->right_blocks, x, 0, z)) &&
!is_transparent(getArrayByte3D(state->blocks, x, y, z+1))) {
return 1;
}
} else {
return 1;
}
}
if ( (x == 0) && (y == 15) ) {
if ((state->left_blocks != Py_None) &&
(state->right_blocks != Py_None)) {
if (!is_transparent(getArrayByte3D(state->left_blocks, 15, y, z)) &&
!is_transparent(getArrayByte3D(state->right_blocks, x, 0, z)) &&
!is_transparent(getArrayByte3D(state->blocks, x, y, z+1))) {
return 1;
}
} else {
return 1;
}
}
if ( (x != 0) && (y != 15) &&
!is_transparent(getArrayByte3D(state->blocks, x-1, y, z)) &&
!is_transparent(getArrayByte3D(state->blocks, x, y, z+1)) &&
!is_transparent(getArrayByte3D(state->blocks, x, y+1, z))) {
return 1;
}
} else { /* if z == 127 skip */
return 1;
}
/* check for lakes and seas and don't render them */
/* at this point of the code the block has no skylight
* and is not occluded, but a deep sea can fool these
* 2 tests */
if ((getArrayByte3D(state->blocks, x, y, z) == 9) ||
(getArrayByte3D(state->blocks, x, y, z+1) == 9)) {
for (dz = z+1; dz < 127; dz++) { /* go up and check for skylight */
if (getArrayByte3D(self->skylight, x, y, dz) != 0) {
return 1;
}
if (getArrayByte3D(state->blocks, x, y, dz) != 9) {
/* we are out of the water! and there's no skylight
* , i.e. is a cave lake or something similar */
return 0;
}
}
}
return 0;
}
static int
rendermode_cave_start(void *data, RenderState *state) {
RenderModeCave* self;
int ret;
self = (RenderModeCave *)data;
/* first, chain up */
ret = rendermode_normal.start(data, state);
if (ret != 0)
return ret;
/* if there's skylight we are in the surface! */
self->skylight = PyObject_GetAttrString(state->self, "skylight");
self->left_skylight = PyObject_GetAttrString(state->self, "left_skylight");
self->right_skylight = PyObject_GetAttrString(state->self, "right_skylight");
self->up_left_skylight = PyObject_GetAttrString(state->self, "up_left_skylight");
self->up_right_skylight = PyObject_GetAttrString(state->self, "up_right_skylight");
/* colors for tinting */
self->depth_colors = PyObject_GetAttrString(state->chunk, "depth_colors");
return 0;
}
static void
rendermode_cave_finish(void *data, RenderState *state) {
RenderModeCave* self;
self = (RenderModeCave *)data;
Py_DECREF(self->skylight);
Py_DECREF(self->left_skylight);
Py_DECREF(self->right_skylight);
Py_DECREF(self->up_left_skylight);
Py_DECREF(self->up_right_skylight);
Py_DECREF(self->depth_colors);
rendermode_normal.finish(data, state);
}
static void
rendermode_cave_draw(void *data, RenderState *state, PyObject *src, PyObject *mask, PyObject *mask_light) {
RenderModeCave* self;
int z, r, g, b;
self = (RenderModeCave *)data;
z = state->z;
r = 0, g = 0, b = 0;
/* draw the normal block */
rendermode_normal.draw(data, state, src, mask, mask_light);
/* get the colors and tint and tint */
/* TODO TODO for a nether mode there isn't tinting! */
r = PyInt_AsLong(PyList_GetItem(self->depth_colors, 0 + z*3));
g = PyInt_AsLong(PyList_GetItem(self->depth_colors, 1 + z*3));
b = PyInt_AsLong(PyList_GetItem(self->depth_colors, 2 + z*3));
tint_with_mask(state->img, r, g, b, 255, mask, state->imgx, state->imgy, 0, 0);
}
RenderModeInterface rendermode_cave = {
"cave", "render only caves in normal mode",
&rendermode_normal,
sizeof(RenderModeCave),
rendermode_cave_start,
rendermode_cave_finish,
rendermode_cave_occluded,
rendermode_cave_draw,
};

View File

@@ -0,0 +1,315 @@
/*
* 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/>.
*/
#include "overviewer.h"
#include <math.h>
/* figures out the black_coeff from a given skylight and blocklight,
used in lighting calculations */
static float calculate_darkness(unsigned char skylight, unsigned char blocklight) {
return 1.0f - powf(0.8f, 15.0 - MAX(blocklight, skylight));
}
/* loads the appropriate light data for the given (possibly non-local)
* coordinates, and returns a black_coeff this is exposed, so other (derived)
* rendermodes can use it
*
* authoratative is a return slot for whether or not this lighting calculation
* is true, or a guess. If we guessed, *authoratative will be false, but if it
* was calculated correctly from available light data, it will be true. You
* may (and probably should) pass NULL.
*/
inline unsigned char
estimate_blocklevel(RenderModeLighting *self, RenderState *state,
int x, int y, int z, int *authoratative) {
/* placeholders for later data arrays, coordinates */
PyObject *blocks = NULL;
PyObject *blocklight = NULL;
int local_x = x, local_y = y, local_z = z;
unsigned char block, blocklevel;
unsigned int average_count = 0, average_gather = 0, coeff = 0;
/* defaults to "guess" until told otherwise */
if (authoratative)
*authoratative = 0;
/* find out what chunk we're in, and translate accordingly */
if (x >= 0 && y < 16) {
blocks = state->blocks;
blocklight = self->blocklight;
} else if (x < 0) {
local_x += 16;
blocks = state->left_blocks;
blocklight = self->left_blocklight;
} else if (y >= 16) {
local_y -= 16;
blocks = state->right_blocks;
blocklight = self->right_blocklight;
}
/* make sure we have correctly-ranged coordinates */
if (!(local_x >= 0 && local_x < 16 &&
local_y >= 0 && local_y < 16 &&
local_z >= 0 && local_z < 128)) {
return 0;
}
/* also, make sure we have enough info to correctly calculate lighting */
if (blocks == Py_None || blocks == NULL ||
blocklight == Py_None || blocklight == NULL) {
return 0;
}
block = getArrayByte3D(blocks, local_x, local_y, local_z);
if (authoratative == NULL) {
int auth;
/* iterate through all surrounding blocks to take an average */
int dx, dy, dz, local_block;
for (dx = -1; dx <= 1; dx += 2) {
for (dy = -1; dy <= 1; dy += 2) {
for (dz = -1; dz <= 1; dz += 2) {
/* skip if block is out of range */
if (x+dx < 0 || x+dx >= 16 ||
y+dy < 0 || y+dy >= 16 ||
z+dz < 0 || z+dz >= 128) {
continue;
}
coeff = estimate_blocklevel(self, state, x+dx, y+dy, z+dz, &auth);
local_block = getArrayByte3D(blocks, x+dx, y+dy, z+dz);
/* only add if the block is transparent, this seems to look better than
using every block */
if (auth && is_transparent(local_block)) {
average_gather += coeff;
average_count++;
}
}
}
}
}
/* only return the average if at least one was authoratative */
if (average_count > 0) {
return average_gather / average_count;
}
blocklevel = getArrayByte3D(blocklight, local_x, local_y, local_z);
/* no longer a guess */
if (!(block == 44 || block == 53 || block == 67) && authoratative) {
*authoratative = 1;
}
return blocklevel;
}
inline float
get_lighting_coefficient(RenderModeLighting *self, RenderState *state,
int x, int y, int z) {
/* placeholders for later data arrays, coordinates */
PyObject *blocks = NULL;
PyObject *skylight = NULL;
PyObject *blocklight = NULL;
int local_x = x, local_y = y, local_z = z;
unsigned char block, skylevel, blocklevel;
/* find out what chunk we're in, and translate accordingly */
if (x >= 0 && y < 16) {
blocks = state->blocks;
skylight = self->skylight;
blocklight = self->blocklight;
} else if (x < 0) {
local_x += 16;
blocks = state->left_blocks;
skylight = self->left_skylight;
blocklight = self->left_blocklight;
} else if (y >= 16) {
local_y -= 16;
blocks = state->right_blocks;
skylight = self->right_skylight;
blocklight = self->right_blocklight;
}
/* make sure we have correctly-ranged coordinates */
if (!(local_x >= 0 && local_x < 16 &&
local_y >= 0 && local_y < 16 &&
local_z >= 0 && local_z < 128)) {
return self->calculate_darkness(15, 0);
}
/* also, make sure we have enough info to correctly calculate lighting */
if (blocks == Py_None || blocks == NULL ||
skylight == Py_None || skylight == NULL ||
blocklight == Py_None || blocklight == NULL) {
return self->calculate_darkness(15, 0);
}
block = getArrayByte3D(blocks, local_x, local_y, local_z);
/* if this block is opaque, use a fully-lit coeff instead
to prevent stippled lines along chunk boundaries! */
if (!is_transparent(block)) {
return self->calculate_darkness(15, 0);
}
skylevel = getArrayByte3D(skylight, local_x, local_y, local_z);
blocklevel = getArrayByte3D(blocklight, local_x, local_y, local_z);
/* special half-step handling */
if (block == 44 || block == 53 || block == 67) {
unsigned int upper_block;
/* stairs and half-blocks take the skylevel from the upper block if it's transparent */
if (local_z != 127) {
upper_block = getArrayByte3D(blocks, local_x, local_y, local_z + 1);
if (is_transparent(upper_block)) {
skylevel = getArrayByte3D(skylight, local_x, local_y, local_z + 1);
}
} else {
upper_block = 0;
skylevel = 15;
}
/* the block has a bad blocklevel, estimate it from neigborhood
/* use given coordinates, no local ones! */
blocklevel = estimate_blocklevel(self, state, x, y, z, NULL);
}
if (block == 10 || block == 11) {
/* lava blocks should always be lit! */
return 0.0f;
}
return self->calculate_darkness(skylevel, blocklevel);
}
/* shades the drawn block with the given facemask/black_color, based on the
lighting results from (x, y, z) */
static inline void
do_shading_with_mask(RenderModeLighting *self, RenderState *state,
int x, int y, int z, PyObject *mask) {
float black_coeff;
/* first, check for occlusion if the block is in the local chunk */
if (x >= 0 && x < 16 && y >= 0 && y < 16 && z >= 0 && z < 128) {
unsigned char block = getArrayByte3D(state->blocks, x, y, z);
if (!is_transparent(block)) {
/* this face isn't visible, so don't draw anything */
return;
}
}
black_coeff = get_lighting_coefficient(self, state, x, y, z);
alpha_over_full(state->img, self->black_color, mask, black_coeff, state->imgx, state->imgy, 0, 0);
}
static int
rendermode_lighting_start(void *data, RenderState *state) {
RenderModeLighting* self;
/* first, chain up */
int ret = rendermode_normal.start(data, state);
if (ret != 0)
return ret;
self = (RenderModeLighting *)data;
self->black_color = PyObject_GetAttrString(state->chunk, "black_color");
self->facemasks_py = PyObject_GetAttrString(state->chunk, "facemasks");
// borrowed references, don't need to be decref'd
self->facemasks[0] = PyTuple_GetItem(self->facemasks_py, 0);
self->facemasks[1] = PyTuple_GetItem(self->facemasks_py, 1);
self->facemasks[2] = PyTuple_GetItem(self->facemasks_py, 2);
self->skylight = PyObject_GetAttrString(state->self, "skylight");
self->blocklight = PyObject_GetAttrString(state->self, "blocklight");
self->left_skylight = PyObject_GetAttrString(state->self, "left_skylight");
self->left_blocklight = PyObject_GetAttrString(state->self, "left_blocklight");
self->right_skylight = PyObject_GetAttrString(state->self, "right_skylight");
self->right_blocklight = PyObject_GetAttrString(state->self, "right_blocklight");
self->calculate_darkness = calculate_darkness;
return 0;
}
static void
rendermode_lighting_finish(void *data, RenderState *state) {
RenderModeLighting *self = (RenderModeLighting *)data;
Py_DECREF(self->black_color);
Py_DECREF(self->facemasks_py);
Py_DECREF(self->skylight);
Py_DECREF(self->blocklight);
Py_DECREF(self->left_skylight);
Py_DECREF(self->left_blocklight);
Py_DECREF(self->right_skylight);
Py_DECREF(self->right_blocklight);
/* now chain up */
rendermode_normal.finish(data, state);
}
static int
rendermode_lighting_occluded(void *data, RenderState *state) {
/* no special occlusion here */
return rendermode_normal.occluded(data, state);
}
static void
rendermode_lighting_draw(void *data, RenderState *state, PyObject *src, PyObject *mask, PyObject *mask_light) {
RenderModeLighting* self;
int x, y, z;
/* first, chain up */
rendermode_normal.draw(data, state, src, mask, mask_light);
self = (RenderModeLighting *)data;
x = state->x, y = state->y, z = state->z;
if (is_transparent(state->block)) {
/* transparent: do shading on whole block */
do_shading_with_mask(self, state, x, y, z, mask_light);
} else {
/* opaque: do per-face shading */
do_shading_with_mask(self, state, x, y, z+1, self->facemasks[0]);
do_shading_with_mask(self, state, x-1, y, z, self->facemasks[1]);
do_shading_with_mask(self, state, x, y+1, z, self->facemasks[2]);
}
}
RenderModeInterface rendermode_lighting = {
"lighting", "draw shadows from the lighting data",
&rendermode_normal,
sizeof(RenderModeLighting),
rendermode_lighting_start,
rendermode_lighting_finish,
rendermode_lighting_occluded,
rendermode_lighting_draw,
};

View File

@@ -0,0 +1,70 @@
/*
* 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/>.
*/
#include "overviewer.h"
#include <math.h>
/* figures out the black_coeff from a given skylight and blocklight, used in
lighting calculations -- note this is *different* from the one in
rendermode-lighting.c (the "skylight - 11" part) */
static float calculate_darkness(unsigned char skylight, unsigned char blocklight) {
return 1.0f - powf(0.8f, 15.0 - MAX(blocklight, skylight - 11));
}
static int
rendermode_night_start(void *data, RenderState *state) {
RenderModeNight* self;
/* first, chain up */
int ret = rendermode_lighting.start(data, state);
if (ret != 0)
return ret;
/* override the darkness function with our night version! */
self = (RenderModeNight *)data;
self->parent.calculate_darkness = calculate_darkness;
return 0;
}
static void
rendermode_night_finish(void *data, RenderState *state) {
/* nothing special to do */
rendermode_lighting.finish(data, state);
}
static int
rendermode_night_occluded(void *data, RenderState *state) {
/* no special occlusion here */
return rendermode_lighting.occluded(data, state);
}
static void
rendermode_night_draw(void *data, RenderState *state, PyObject *src, PyObject *mask, PyObject *mask_light) {
/* nothing special to do */
rendermode_lighting.draw(data, state, src, mask, mask_light);
}
RenderModeInterface rendermode_night = {
"night", "like \"lighting\", except at night",
&rendermode_lighting,
sizeof(RenderModeNight),
rendermode_night_start,
rendermode_night_finish,
rendermode_night_occluded,
rendermode_night_draw,
};

View File

@@ -0,0 +1,256 @@
/*
* 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/>.
*/
#include "overviewer.h"
static int
rendermode_normal_start(void *data, RenderState *state) {
PyObject *chunk_x_py, *chunk_y_py, *world, *use_biomes, *worlddir;
RenderModeNormal *self = (RenderModeNormal *)data;
chunk_x_py = PyObject_GetAttrString(state->self, "chunkX");
chunk_y_py = PyObject_GetAttrString(state->self, "chunkY");
/* careful now -- C's % operator works differently from python's
we can't just do x % 32 like we did before */
self->chunk_x = PyInt_AsLong(chunk_x_py);
self->chunk_y = PyInt_AsLong(chunk_y_py);
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;
/* fetch the biome data from textures.py, if needed */
world = PyObject_GetAttrString(state->self, "world");
worlddir = PyObject_GetAttrString(world, "worlddir");
use_biomes = PyObject_GetAttrString(world, "useBiomeData");
Py_DECREF(world);
if (PyObject_IsTrue(use_biomes)) {
PyObject *facemasks_py;
self->biome_data = PyObject_CallMethod(state->textures, "getBiomeData", "OOO",
worlddir, chunk_x_py, chunk_y_py);
if (self->biome_data == Py_None) {
self->biome_data = NULL;
self->foliagecolor = NULL;
self->grasscolor = NULL;
self->leaf_texture = NULL;
self->grass_texture = NULL;
self->tall_grass_texture = NULL;
self->facemask_top = NULL;
} else {
self->foliagecolor = PyObject_GetAttrString(state->textures, "foliagecolor");
self->grasscolor = PyObject_GetAttrString(state->textures, "grasscolor");
self->leaf_texture = PyObject_GetAttrString(state->textures, "biome_leaf_texture");
self->grass_texture = PyObject_GetAttrString(state->textures, "biome_grass_texture");
self->tall_grass_texture = PyObject_GetAttrString(state->textures, "biome_tall_grass_texture");
self->tall_fern_texture = PyObject_GetAttrString(state->textures, "biome_tall_fern_texture");
facemasks_py = PyObject_GetAttrString(state->chunk, "facemasks");
/* borrowed reference, needs to be incref'd if we keep it */
self->facemask_top = PyTuple_GetItem(facemasks_py, 0);
Py_INCREF(self->facemask_top);
Py_DECREF(facemasks_py);
}
} else {
self->biome_data = NULL;
self->foliagecolor = NULL;
self->grasscolor = NULL;
self->leaf_texture = NULL;
self->grass_texture = NULL;
self->tall_grass_texture = NULL;
self->tall_fern_texture = NULL;
self->facemask_top = NULL;
}
Py_DECREF(use_biomes);
Py_DECREF(worlddir);
Py_DECREF(chunk_x_py);
Py_DECREF(chunk_y_py);
return 0;
}
static void
rendermode_normal_finish(void *data, RenderState *state) {
RenderModeNormal *self = (RenderModeNormal *)data;
Py_XDECREF(self->biome_data);
Py_XDECREF(self->foliagecolor);
Py_XDECREF(self->grasscolor);
Py_XDECREF(self->leaf_texture);
Py_XDECREF(self->grass_texture);
Py_XDECREF(self->tall_grass_texture);
Py_XDECREF(self->tall_fern_texture);
Py_XDECREF(self->facemask_top);
}
static int
rendermode_normal_occluded(void *data, RenderState *state) {
int x = state->x, y = state->y, z = state->z;
if ( (x != 0) && (y != 15) && (z != 127) &&
!is_transparent(getArrayByte3D(state->blocks, x-1, y, z)) &&
!is_transparent(getArrayByte3D(state->blocks, x, y, z+1)) &&
!is_transparent(getArrayByte3D(state->blocks, x, y+1, z))) {
return 1;
}
return 0;
}
static void
rendermode_normal_draw(void *data, RenderState *state, PyObject *src, PyObject *mask, PyObject *mask_light) {
RenderModeNormal *self = (RenderModeNormal *)data;
int randx = 0,randy = 0;
unsigned char data_byte;
/* first, check to see if we should use biome-compatible src, mask */
if (self->biome_data) {
if (state->block == 18) {
src = mask = self->leaf_texture;
} else if (state->block == 31) {
/* add a random offset to the postion of the tall grass to make it more wild */
randx = rand() % 6 + 1 - 3;
randy = rand() % 6 + 1 - 3;
state->imgx = state->imgx + randx;
state->imgy = state->imgy + randy;
data_byte = getArrayByte3D(state->blockdata_expanded, state->x, state->y, state->z);
if (data_byte == 1) {
src = mask = self->tall_grass_texture;
} else if (data_byte == 2) {
src = mask = self->tall_fern_texture;
}
}
}
/* draw the block! */
alpha_over(state->img, src, mask, state->imgx, state->imgy, 0, 0);
if (self->biome_data) {
/* do the biome stuff! */
unsigned int index;
PyObject *color = NULL, *facemask = NULL;
unsigned char r, g, b;
index = ((self->chunk_y * 16) + state->y) * 16 * 32 + (self->chunk_x * 16) + state->x;
index = big_endian_ushort(getArrayShort1D(self->biome_data, index));
switch (state->block) {
case 2:
/* grass -- skip for snowgrass */
if (state->z < 127 && getArrayByte3D(state->blocks, state->x, state->y, state->z+1) == 78)
break;
color = PySequence_GetItem(self->grasscolor, index);
facemask = self->grass_texture;
alpha_over(state->img, self->grass_texture, self->grass_texture, state->imgx, state->imgy, 0, 0);
break;
case 18:
/* leaves */
color = PySequence_GetItem(self->foliagecolor, index);
facemask = mask;
break;
case 31:
/* tall grass */
if ( getArrayByte3D(state->blockdata_expanded, state->x, state->y, state->z) != 0 )
{ /* do not tint dead shrubs */
color = PySequence_GetItem(self->grasscolor, index);
facemask = mask;
break;
}
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);
tint_with_mask(state->img, r, g, b, 255, facemask, state->imgx, state->imgy, 0, 0);
}
}
/* Draw some edge lines! */
// draw.line(((imgx+12,imgy+increment), (imgx+22,imgy+5+increment)), fill=(0,0,0), width=1)
if (state->block == 44 || state->block == 78 || !is_transparent(state->block)) {
Imaging img_i = imaging_python_to_c(state->img);
unsigned char ink[] = {0,0,0,40};
int increment=0;
if (state->block == 44) // half-step
increment=6;
else if ((state->block == 78) || (state->block == 93) || (state->block == 94)) // snow, redstone repeaters (on and off)
increment=9;
if ((state->x == 15) && (state->up_right_blocks != Py_None)) {
unsigned char side_block = getArrayByte3D(state->up_right_blocks, 0, state->y, state->z);
if (side_block != state->block && is_transparent(side_block)) {
ImagingDrawLine(img_i, state->imgx+12, state->imgy+1+increment, state->imgx+22+1, state->imgy+5+1+increment, &ink, 1);
ImagingDrawLine(img_i, state->imgx+12, state->imgy+increment, state->imgx+22+1, state->imgy+5+increment, &ink, 1);
}
} else if (state->x != 15) {
unsigned char side_block = getArrayByte3D(state->blocks, state->x+1, state->y, state->z);
if (side_block != state->block && is_transparent(side_block)) {
ImagingDrawLine(img_i, state->imgx+12, state->imgy+1+increment, state->imgx+22+1, state->imgy+5+1+increment, &ink, 1);
ImagingDrawLine(img_i, state->imgx+12, state->imgy+increment, state->imgx+22+1, state->imgy+5+increment, &ink, 1);
}
}
// if y != 0 and blocks[x,y-1,z] == 0
// chunk boundries are annoying
if ((state->y == 0) && (state->up_left_blocks != Py_None)) {
unsigned char side_block = getArrayByte3D(state->up_left_blocks, state->x, 15, state->z);
if (side_block != state->block && is_transparent(side_block)) {
ImagingDrawLine(img_i, state->imgx, state->imgy+6+1+increment, state->imgx+12+1, state->imgy+1+increment, &ink, 1);
ImagingDrawLine(img_i, state->imgx, state->imgy+6+increment, state->imgx+12+1, state->imgy+increment, &ink, 1);
}
} else if (state->y != 0) {
unsigned char side_block = getArrayByte3D(state->blocks, state->x, state->y-1, state->z);
if (side_block != state->block && is_transparent(side_block)) {
// draw.line(((imgx,imgy+6+increment), (imgx+12,imgy+increment)), fill=(0,0,0), width=1)
ImagingDrawLine(img_i, state->imgx, state->imgy+6+1+increment, state->imgx+12+1, state->imgy+1+increment, &ink, 1);
ImagingDrawLine(img_i, state->imgx, state->imgy+6+increment, state->imgx+12+1, state->imgy+increment, &ink, 1);
}
}
}
}
RenderModeInterface rendermode_normal = {
"normal", "nothing special, just render the blocks",
NULL,
sizeof(RenderModeNormal),
rendermode_normal_start,
rendermode_normal_finish,
rendermode_normal_occluded,
rendermode_normal_draw,
};

View File

@@ -0,0 +1,137 @@
/*
* 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/>.
*/
#include "overviewer.h"
static void get_color(void *data, RenderState *state,
unsigned char *r, unsigned char *g, unsigned char *b, unsigned char *a) {
*r = 200;
*g = 200;
*b = 255;
*a = 155;
}
static int
rendermode_overlay_start(void *data, RenderState *state) {
PyObject *facemasks_py;
RenderModeOverlay *self = (RenderModeOverlay *)data;
facemasks_py = PyObject_GetAttrString(state->chunk, "facemasks");
/* borrowed reference, needs to be incref'd if we keep it */
self->facemask_top = PyTuple_GetItem(facemasks_py, 0);
Py_INCREF(self->facemask_top);
Py_DECREF(facemasks_py);
self->white_color = PyObject_GetAttrString(state->chunk, "white_color");
self->solid_blocks = PyObject_GetAttrString(state->chunk, "solid_blocks");
self->fluid_blocks = PyObject_GetAttrString(state->chunk, "fluid_blocks");
self->get_color = get_color;
return 0;
}
static void
rendermode_overlay_finish(void *data, RenderState *state) {
RenderModeOverlay *self = (RenderModeOverlay *)data;
Py_DECREF(self->facemask_top);
Py_DECREF(self->white_color);
Py_DECREF(self->solid_blocks);
Py_DECREF(self->fluid_blocks);
}
static int
rendermode_overlay_occluded(void *data, RenderState *state) {
int x = state->x, y = state->y, z = state->z;
if ( (x != 0) && (y != 15) && (z != 127) &&
!is_transparent(getArrayByte3D(state->blocks, x-1, y, z)) &&
!is_transparent(getArrayByte3D(state->blocks, x, y, z+1)) &&
!is_transparent(getArrayByte3D(state->blocks, x, y+1, z))) {
return 1;
}
return 0;
}
static void
rendermode_overlay_draw(void *data, RenderState *state, PyObject *src, PyObject *mask, PyObject *mask_light) {
RenderModeOverlay *self = (RenderModeOverlay *)data;
unsigned char r, g, b, a;
PyObject *top_block_py, *block_py;
// exactly analogous to edge-line code for these special blocks
int increment=0;
if (state->block == 44) // half-step
increment=6;
else if (state->block == 78) // snow
increment=9;
/* clear the draw space -- set alpha to 0 within mask */
tint_with_mask(state->img, 255, 255, 255, 0, mask, state->imgx, state->imgy, 0, 0);
/* skip rendering the overlay if we can't see it */
if (state->z != 127) {
unsigned char top_block = getArrayByte3D(state->blocks, state->x, state->y, state->z+1);
if (!is_transparent(top_block)) {
return;
}
/* check to be sure this block is solid/fluid */
top_block_py = PyInt_FromLong(top_block);
if (PySequence_Contains(self->solid_blocks, top_block_py) ||
PySequence_Contains(self->fluid_blocks, top_block_py)) {
/* top block is fluid or solid, skip drawing */
Py_DECREF(top_block_py);
return;
}
Py_DECREF(top_block_py);
}
/* check to be sure this block is solid/fluid */
block_py = PyInt_FromLong(state->block);
if (!PySequence_Contains(self->solid_blocks, block_py) &&
!PySequence_Contains(self->fluid_blocks, block_py)) {
/* not fluid or solid, skip drawing the overlay */
Py_DECREF(block_py);
return;
}
Py_DECREF(block_py);
/* get our color info */
self->get_color(data, state, &r, &g, &b, &a);
/* do the overlay */
if (a > 0) {
alpha_over(state->img, self->white_color, self->facemask_top, state->imgx, state->imgy + increment, 0, 0);
tint_with_mask(state->img, r, g, b, a, self->facemask_top, state->imgx, state->imgy + increment, 0, 0);
}
}
RenderModeInterface rendermode_overlay = {
"overlay", "base rendermode for informational overlays",
NULL,
sizeof(RenderModeOverlay),
rendermode_overlay_start,
rendermode_overlay_finish,
rendermode_overlay_occluded,
rendermode_overlay_draw,
};

View File

@@ -0,0 +1,118 @@
/*
* 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/>.
*/
#include "overviewer.h"
#include <math.h>
static void get_color(void *data, RenderState *state,
unsigned char *r, unsigned char *g, unsigned char *b, unsigned char *a) {
RenderModeSpawn* self = (RenderModeSpawn *)data;
int x = state->x, y = state->y, z = state->z;
int z_light = z + 1;
unsigned char blocklight, skylight;
PyObject *block_py;
/* set a nice, pretty red color */
*r = 229;
*g = 36;
*b = 38;
/* default to no overlay, until told otherwise */
*a = 0;
block_py = PyInt_FromLong(state->block);
if (PySequence_Contains(self->nospawn_blocks, block_py)) {
/* nothing can spawn on this */
Py_DECREF(block_py);
return;
}
Py_DECREF(block_py);
blocklight = getArrayByte3D(self->blocklight, x, y, MIN(127, z_light));
/* if we're at the top, force 15 (brightest!) skylight */
if (z_light == 128) {
skylight = 15;
} else {
skylight = getArrayByte3D(self->skylight, x, y, z_light);
}
if (MAX(blocklight, skylight) <= 7) {
/* hostile mobs spawn in daylight */
*a = 240;
} else if (MAX(blocklight, skylight - 11) <= 7) {
/* hostile mobs spawn at night */
*a = 150;
}
}
static int
rendermode_spawn_start(void *data, RenderState *state) {
RenderModeSpawn* self;
/* first, chain up */
int ret = rendermode_overlay.start(data, state);
if (ret != 0)
return ret;
/* now do custom initializations */
self = (RenderModeSpawn *)data;
self->nospawn_blocks = PyObject_GetAttrString(state->chunk, "nospawn_blocks");
self->blocklight = PyObject_GetAttrString(state->self, "blocklight");
self->skylight = PyObject_GetAttrString(state->self, "skylight");
/* setup custom color */
self->parent.get_color = get_color;
return 0;
}
static void
rendermode_spawn_finish(void *data, RenderState *state) {
/* first free all *our* stuff */
RenderModeSpawn* self = (RenderModeSpawn *)data;
Py_DECREF(self->nospawn_blocks);
Py_DECREF(self->blocklight);
Py_DECREF(self->skylight);
/* now, chain up */
rendermode_overlay.finish(data, state);
}
static int
rendermode_spawn_occluded(void *data, RenderState *state) {
/* no special occlusion here */
return rendermode_overlay.occluded(data, state);
}
static void
rendermode_spawn_draw(void *data, RenderState *state, PyObject *src, PyObject *mask, PyObject *mask_light) {
/* draw normally */
rendermode_overlay.draw(data, state, src, mask, mask_light);
}
RenderModeInterface rendermode_spawn = {
"spawn", "draws a red overlay where monsters can spawn at night",
&rendermode_overlay,
sizeof(RenderModeSpawn),
rendermode_spawn_start,
rendermode_spawn_finish,
rendermode_spawn_occluded,
rendermode_spawn_draw,
};

View File

@@ -0,0 +1,184 @@
/*
* 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/>.
*/
#include "overviewer.h"
#include <string.h>
/* list of all render modes, ending in NULL
all of these will be available to the user, so DON'T include modes
that are only useful as a base for other modes. */
static RenderModeInterface *render_modes[] = {
&rendermode_normal,
&rendermode_lighting,
&rendermode_night,
&rendermode_spawn,
&rendermode_cave,
NULL
};
/* decides which render mode to use */
RenderModeInterface *get_render_mode(RenderState *state) {
unsigned int i;
/* default: NULL --> an error */
RenderModeInterface *iface = NULL;
PyObject *rendermode_py = PyObject_GetAttrString(state->self, "rendermode");
const char *rendermode = PyString_AsString(rendermode_py);
for (i = 0; render_modes[i] != NULL; i++) {
if (strcmp(render_modes[i]->name, rendermode) == 0) {
iface = render_modes[i];
break;
}
}
Py_DECREF(rendermode_py);
return iface;
}
/* bindings for python -- get all the rendermode names */
PyObject *get_render_modes(PyObject *self, PyObject *args) {
PyObject *modes;
unsigned int i;
if (!PyArg_ParseTuple(args, ""))
return NULL;
modes = PyList_New(0);
if (modes == NULL)
return NULL;
for (i = 0; render_modes[i] != NULL; i++) {
PyObject *name = PyString_FromString(render_modes[i]->name);
PyList_Append(modes, name);
Py_DECREF(name);
}
return modes;
}
/* more bindings -- return info for a given rendermode name */
PyObject *get_render_mode_info(PyObject *self, PyObject *args) {
const char* rendermode;
PyObject *info;
unsigned int i;
if (!PyArg_ParseTuple(args, "s", &rendermode))
return NULL;
info = PyDict_New();
if (info == NULL)
return NULL;
for (i = 0; render_modes[i] != NULL; i++) {
if (strcmp(render_modes[i]->name, rendermode) == 0) {
PyObject *tmp;
tmp = PyString_FromString(render_modes[i]->name);
PyDict_SetItemString(info, "name", tmp);
Py_DECREF(tmp);
tmp = PyString_FromString(render_modes[i]->description);
PyDict_SetItemString(info, "description", tmp);
Py_DECREF(tmp);
return info;
}
}
Py_DECREF(info);
return PyErr_Format(PyExc_ValueError, "invalid rendermode: \"%s\"", rendermode);
}
/* bindings -- get parent's name */
PyObject *get_render_mode_parent(PyObject *self, PyObject *args) {
const char *rendermode;
unsigned int i;
if (!PyArg_ParseTuple(args, "s", &rendermode))
return NULL;
for (i = 0; render_modes[i] != NULL; i++) {
if (strcmp(render_modes[i]->name, rendermode) == 0) {
if (render_modes[i]->parent) {
/* has parent */
return PyString_FromString(render_modes[i]->parent->name);
} else {
/* no parent */
Py_RETURN_NONE;
}
}
}
return PyErr_Format(PyExc_ValueError, "invalid rendermode: \"%s\"", rendermode);
}
/* bindings -- get list of inherited parents */
PyObject *get_render_mode_inheritance(PyObject *self, PyObject *args) {
const char *rendermode;
PyObject *parents;
unsigned int i;
RenderModeInterface *iface = NULL;
if (!PyArg_ParseTuple(args, "s", &rendermode))
return NULL;
parents = PyList_New(0);
if (!parents)
return NULL;
for (i = 0; render_modes[i] != NULL; i++) {
if (strcmp(render_modes[i]->name, rendermode) == 0) {
iface = render_modes[i];
break;
}
}
if (!iface) {
Py_DECREF(parents);
return PyErr_Format(PyExc_ValueError, "invalid rendermode: \"%s\"", rendermode);
}
while (iface) {
PyObject *name = PyString_FromString(iface->name);
PyList_Append(parents, name);
Py_DECREF(name);
iface = iface->parent;
}
PyList_Reverse(parents);
return parents;
}
/* bindings -- get list of (direct) children */
PyObject *get_render_mode_children(PyObject *self, PyObject *args) {
const char *rendermode;
PyObject *children;
unsigned int i;
if (!PyArg_ParseTuple(args, "s", &rendermode))
return NULL;
children = PyList_New(0);
if (!children)
return NULL;
for (i = 0; render_modes[i] != NULL; i++) {
if (render_modes[i]->parent && strcmp(render_modes[i]->parent->name, rendermode) == 0) {
PyObject *child_name = PyString_FromString(render_modes[i]->name);
PyList_Append(children, child_name);
Py_DECREF(child_name);
}
}
return children;
}

View File

@@ -0,0 +1,159 @@
/*
* 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/>.
*/
/*
* To make a new render mode (the C part, at least):
*
* * add a data struct and extern'd interface declaration below
*
* * fill in this interface struct in rendermode-(yourmode).c
* (see rendermodes-normal.c for an example: the "normal" mode)
*
* * if you want to derive from (say) the "normal" mode, put
* a RenderModeNormal entry at the top of your data struct, and
* be sure to call your parent's functions in your own!
* (see rendermode-night.c for a simple example derived from
* the "lighting" mode)
*
* * add your mode to the list in rendermodes.c
*/
#ifndef __RENDERMODES_H_INCLUDED__
#define __RENDERMODES_H_INCLUDED__
#include <Python.h>
/* rendermode interface */
typedef struct _RenderModeInterface RenderModeInterface;
struct _RenderModeInterface {
/* the name of this mode */
const char* name;
/* the short description of this render mode */
const char* description;
/* the rendermode this is derived from, or NULL */
RenderModeInterface *parent;
/* the size of the local storage for this rendermode */
unsigned int data_size;
/* may return non-zero on error */
int (*start)(void *, RenderState *);
void (*finish)(void *, RenderState *);
/* returns non-zero to skip rendering this block */
int (*occluded)(void *, RenderState *);
/* last two arguments are img and mask, from texture lookup */
void (*draw)(void *, RenderState *, PyObject *, PyObject *, PyObject *);
};
/* figures out the render mode to use from the given ChunkRenderer */
RenderModeInterface *get_render_mode(RenderState *state);
/* python bindings */
PyObject *get_render_modes(PyObject *self, PyObject *args);
PyObject *get_render_mode_info(PyObject *self, PyObject *args);
PyObject *get_render_mode_parent(PyObject *self, PyObject *args);
PyObject *get_render_mode_inheritance(PyObject *self, PyObject *args);
PyObject *get_render_mode_children(PyObject *self, PyObject *args);
/* individual rendermode interface declarations follow */
/* NORMAL */
typedef struct {
/* coordinates of the chunk, inside its region file */
int chunk_x, chunk_y;
/* biome data for the region */
PyObject *biome_data;
/* grasscolor and foliagecolor lookup tables */
PyObject *grasscolor, *foliagecolor;
/* biome-compatible grass/leaf textures */
PyObject *grass_texture, *leaf_texture, *tall_grass_texture, *tall_fern_texture;
/* top facemask for grass biome tinting */
PyObject *facemask_top;
} RenderModeNormal;
extern RenderModeInterface rendermode_normal;
/* OVERLAY */
typedef struct {
/* top facemask and white color image, for drawing overlays */
PyObject *facemask_top, *white_color;
/* only show overlay on top of solid or fluid blocks */
PyObject *solid_blocks, *fluid_blocks;
/* can be overridden in derived classes to control
overlay alpha and color
last four vars are r, g, b, a out */
void (*get_color)(void *, RenderState *,
unsigned char *, unsigned char *, unsigned char *, unsigned char *);
} RenderModeOverlay;
extern RenderModeInterface rendermode_overlay;
/* LIGHTING */
typedef struct {
/* inherits from normal render mode */
RenderModeNormal parent;
PyObject *black_color, *facemasks_py;
PyObject *facemasks[3];
/* extra data, loaded off the chunk class */
PyObject *skylight, *blocklight;
PyObject *left_skylight, *left_blocklight;
PyObject *right_skylight, *right_blocklight;
/* can be overridden in derived rendermodes to control lighting
arguments are skylight, blocklight */
float (*calculate_darkness)(unsigned char, unsigned char);
} RenderModeLighting;
extern RenderModeInterface rendermode_lighting;
inline float get_lighting_coefficient(RenderModeLighting *self, RenderState *state,
int x, int y, int z);
/* NIGHT */
typedef struct {
/* inherits from lighting */
RenderModeLighting parent;
} RenderModeNight;
extern RenderModeInterface rendermode_night;
/* SPAWN */
typedef struct {
/* inherits from overlay */
RenderModeOverlay parent;
/* used to figure out which blocks are spawnable */
PyObject *nospawn_blocks;
PyObject *skylight, *blocklight;
} RenderModeSpawn;
extern RenderModeInterface rendermode_spawn;
/* CAVE */
typedef struct {
/* render blocks with lighting mode */
RenderModeNormal parent;
/* data used to know where the surface is */
PyObject *skylight;
PyObject *left_skylight;
PyObject *right_skylight;
PyObject *up_left_skylight;
PyObject *up_right_skylight;
/* colors used for tinting */
PyObject *depth_colors;
} RenderModeCave;
extern RenderModeInterface rendermode_cave;
#endif /* __RENDERMODES_H_INCLUDED__ */

1698
overviewer_core/textures.py Normal file

File diff suppressed because it is too large Load Diff

74
overviewer_core/util.py Normal file
View File

@@ -0,0 +1,74 @@
# 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/>.
"""
Misc utility routines used by multiple files that don't belong anywhere else
"""
import imp
import os
import os.path
import sys
from subprocess import Popen, PIPE
def get_program_path():
if hasattr(sys, "frozen") or imp.is_frozen("__main__"):
return os.path.dirname(sys.executable)
else:
try:
# normally, we're in ./overviewer_core/util.py
# we want ./
return os.path.dirname(os.path.dirname(__file__))
except NameError:
return os.path.dirname(sys.argv[0])
# does not require git, very likely to work everywhere
def findGitHash():
this_dir = get_program_path()
if os.path.exists(os.path.join(this_dir,".git")):
with open(os.path.join(this_dir,".git","HEAD")) as f:
data = f.read().strip()
if data.startswith("ref: "):
if not os.path.exists(os.path.join(this_dir, ".git", data[5:])):
return data
with open(os.path.join(this_dir, ".git", data[5:])) as g:
return g.read().strip()
else:
return data
else:
try:
import overviewer_version
return overviewer_version.HASH
except:
return "unknown"
def findGitVersion():
try:
p = Popen(['git', 'describe', '--tags'], stdout=PIPE, stderr=PIPE)
p.stderr.close()
line = p.stdout.readlines()[0]
if line.startswith('release-'):
line = line.split('-', 1)[1]
# turn 0.1.2-50-somehash into 0.1.2-50
# and 0.1.3 into 0.1.3
line = '-'.join(line.split('-', 2)[:2])
return line.strip()
except:
try:
import overviewer_version
return overviewer_version.VERSION
except:
return "unknown"

348
overviewer_core/world.py Normal file
View File

@@ -0,0 +1,348 @@
# 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 functools
import os
import os.path
from glob import glob
import multiprocessing
import Queue
import sys
import logging
import cPickle
import collections
import itertools
import numpy
import chunk
import nbt
import textures
import time
"""
This module has routines for extracting information about available worlds
"""
base36decode = functools.partial(int, base=36)
cached = collections.defaultdict(dict)
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 World(object):
"""Does world-level preprocessing to prepare for QuadtreeGen
worlddir is the path to the minecraft world
"""
mincol = maxcol = minrow = maxrow = 0
def __init__(self, worlddir, useBiomeData=False,regionlist=None):
self.worlddir = worlddir
self.useBiomeData = useBiomeData
#find region files, or load the region list
#this also caches all the region file header info
logging.info("Scanning regions")
regionfiles = {}
self.regions = {}
if regionlist:
self.regionlist = map(os.path.abspath, regionlist) # a list of paths
else:
self.regionlist = None
for x, y, regionfile in self._iterate_regionfiles():
mcr = self.reload_region(regionfile)
mcr.get_chunk_info()
regionfiles[(x,y)] = (x,y,regionfile,mcr)
self.regionfiles = regionfiles
# set the number of region file handles we will permit open at any time before we start closing them
# self.regionlimit = 1000
# the max number of chunks we will keep before removing them (includes emptry chunks)
self.chunklimit = 1024
self.chunkcount = 0
self.empty_chunk = [None,None]
logging.debug("Done scanning regions")
# figure out chunk format is in use
# if not mcregion, error out early
data = nbt.load(os.path.join(self.worlddir, "level.dat"))[1]['Data']
#print data
if not ('version' in data and data['version'] == 19132):
logging.error("Sorry, This version of Minecraft-Overviewer only works with the new McRegion chunk format")
sys.exit(1)
# stores Points Of Interest to be mapped with markers
# a list of dictionaries, see below for an example
self.POI = []
# if it exists, open overviewer.dat, and read in the data structure
# info self.persistentData. This dictionary can hold any information
# that may be needed between runs.
# Currently only holds into about POIs (more more details, see quadtree)
# TODO maybe store this with the tiles, not with the world?
self.pickleFile = os.path.join(self.worlddir, "overviewer.dat")
if os.path.exists(self.pickleFile):
with open(self.pickleFile,"rb") as p:
self.persistentData = cPickle.load(p)
else:
# some defaults
self.persistentData = dict(POI=[])
def get_region_path(self, chunkX, chunkY):
"""Returns the path to the region that contains chunk (chunkX, chunkY)
"""
_, _, regionfile,_ = self.regionfiles.get((chunkX//32, chunkY//32),(None,None,None,None));
return regionfile
def load_from_region(self,filename, x, y):
#we need to manage the chunk cache
regioninfo = self.regions[filename]
if regioninfo is None:
return None
chunks = regioninfo[2]
chunk_data = chunks.get((x,y))
if chunk_data is None:
#prune the cache if required
if self.chunkcount > self.chunklimit: #todo: make the emptying the chunk cache slightly less crazy
[self.reload_region(regionfile) for regionfile in self.regions if regionfile <> filename]
self.chunkcount = 0
self.chunkcount += 1
nbt = self.load_region(filename).load_chunk(x, y)
if nbt is None:
chunks[(x,y)] = self.empty_chunk
return None ## return none. I think this is who we should indicate missing chunks
#raise IOError("No such chunk in region: (%i, %i)" % (x, y))
#we cache the transformed data, not it's raw form
data = nbt.read_all()
level = data[1]['Level']
chunk_data = level
#chunk_data = {}
#chunk_data['skylight'] = chunk.get_skylight_array(level)
#chunk_data['blocklight'] = chunk.get_blocklight_array(level)
#chunk_data['blockarray'] = chunk.get_blockdata_array(level)
#chunk_data['TileEntities'] = chunk.get_tileentity_data(level)
chunks[(x,y)] = [level,time.time()]
else:
chunk_data = chunk_data[0]
return chunk_data
#used to reload a changed region
def reload_region(self,filename):
if self.regions.get(filename) is not None:
self.regions[filename][0].closefile()
chunkcache = {}
mcr = nbt.MCRFileReader(filename)
self.regions[filename] = (mcr,os.path.getmtime(filename),chunkcache)
return mcr
def load_region(self,filename):
return self.regions[filename][0]
def get_region_mtime(self,filename):
return (self.regions[filename][0],self.regions[filename][1])
def convert_coords(self, chunkx, chunky):
"""Takes a coordinate (chunkx, chunky) where chunkx and chunky are
in the chunk coordinate system, and figures out the row and column
in the image each one should be. Returns (col, row)."""
# columns are determined by the sum of the chunk coords, rows are the
# difference (TODO: be able to change direction of north)
# change this function, and you MUST change unconvert_coords
return (chunkx + chunky, chunky - chunkx)
def unconvert_coords(self, col, row):
"""Undoes what convert_coords does. Returns (chunkx, chunky)."""
# col + row = chunky + chunky => (col + row)/2 = chunky
# col - row = chunkx + chunkx => (col - row)/2 = chunkx
return ((col - row) / 2, (col + row) / 2)
def findTrueSpawn(self):
"""Adds the true spawn location to self.POI. The spawn Y coordinate
is almost always the default of 64. Find the first air block above
that point for the true spawn location"""
## read spawn info from level.dat
data = nbt.load(os.path.join(self.worlddir, "level.dat"))[1]
spawnX = data['Data']['SpawnX']
spawnY = data['Data']['SpawnY']
spawnZ = data['Data']['SpawnZ']
## The chunk that holds the spawn location
chunkX = spawnX/16
chunkY = spawnZ/16
## The filename of this chunk
chunkFile = self.get_region_path(chunkX, chunkY)
if chunkFile is not None:
data = nbt.load_from_region(chunkFile, chunkX, chunkY)[1]
if data is not None:
level = data['Level']
blockArray = numpy.frombuffer(level['Blocks'], dtype=numpy.uint8).reshape((16,16,128))
## The block for spawn *within* the chunk
inChunkX = spawnX - (chunkX*16)
inChunkZ = spawnZ - (chunkY*16)
## find the first air block
while (blockArray[inChunkX, inChunkZ, spawnY] != 0):
spawnY += 1
if spawnY == 128:
break
self.POI.append( dict(x=spawnX, y=spawnY, z=spawnZ,
msg="Spawn", type="spawn", chunk=(chunkX, chunkY)))
self.spawn = (spawnX, spawnY, spawnZ)
def go(self, procs):
"""Scan the world directory, to fill in
self.{min,max}{col,row} for use later in quadtree.py. This
also does other world-level processing."""
logging.info("Scanning chunks")
# find the dimensions of the map, in region files
minx = maxx = miny = maxy = 0
found_regions = False
for x, y in self.regionfiles:
found_regions = True
minx = min(minx, x)
maxx = max(maxx, x)
miny = min(miny, y)
maxy = max(maxy, y)
if not found_regions:
logging.error("Error: No chunks found!")
sys.exit(1)
logging.debug("Done scanning chunks")
# turn our region coordinates into chunk coordinates
minx = minx * 32
miny = miny * 32
maxx = maxx * 32 + 32
maxy = maxy * 32 + 32
# Translate chunks to our diagonal coordinate system
mincol = maxcol = minrow = maxrow = 0
for chunkx, chunky in [(minx, miny), (minx, maxy), (maxx, miny), (maxx, maxy)]:
col, row = self.convert_coords(chunkx, chunky)
mincol = min(mincol, col)
maxcol = max(maxcol, col)
minrow = min(minrow, row)
maxrow = max(maxrow, row)
#logging.debug("map size: (%i, %i) to (%i, %i)" % (mincol, minrow, maxcol, maxrow))
self.mincol = mincol
self.maxcol = maxcol
self.minrow = minrow
self.maxrow = maxrow
self.findTrueSpawn()
def _iterate_regionfiles(self,regionlist=None):
"""Returns an iterator of all of the region files, along with their
coordinates
Note: the regionlist here will be used to determinte the size of the
world.
Returns (regionx, regiony, filename)"""
join = os.path.join
if regionlist is not None:
for path in regionlist:
path = path.strip()
f = os.path.basename(path)
if f.startswith("r.") and f.endswith(".mcr"):
p = f.split(".")
logging.debug("Using path %s from regionlist", f)
yield (int(p[1]), int(p[2]), join(self.worlddir, 'region', f))
else:
logging.warning("Ignore path '%s' in regionlist", f)
else:
for path in glob(os.path.join(self.worlddir, 'region') + "/r.*.*.mcr"):
dirpath, f = os.path.split(path)
p = f.split(".")
yield (int(p[1]), int(p[2]), join(dirpath, f))
def get_save_dir():
"""Returns the path to the local saves directory
* On Windows, at %APPDATA%/.minecraft/saves/
* On Darwin, at $HOME/Library/Application Support/minecraft/saves/
* at $HOME/.minecraft/saves/
"""
savepaths = []
if "APPDATA" in os.environ:
savepaths += [os.path.join(os.environ['APPDATA'], ".minecraft", "saves")]
if "HOME" in os.environ:
savepaths += [os.path.join(os.environ['HOME'], "Library",
"Application Support", "minecraft", "saves")]
savepaths += [os.path.join(os.environ['HOME'], ".minecraft", "saves")]
for path in savepaths:
if os.path.exists(path):
return path
def get_worlds():
"Returns {world # or name : level.dat information}"
ret = {}
save_dir = get_save_dir()
# No dirs found - most likely not running from inside minecraft-dir
if save_dir is None:
return None
for dir in os.listdir(save_dir):
world_dat = os.path.join(save_dir, dir, "level.dat")
if not os.path.exists(world_dat): continue
info = nbt.load(world_dat)[1]
info['Data']['path'] = os.path.join(save_dir, dir)
if dir.startswith("World") and len(dir) == 6:
try:
world_n = int(dir[-1])
ret[world_n] = info['Data']
except ValueError:
pass
if 'LevelName' in info['Data'].keys():
ret[info['Data']['LevelName']] = info['Data']
return ret