initial work at reorganizing source tree
This commit is contained in:
0
overviewer/__init__.py
Normal file
0
overviewer/__init__.py
Normal file
505
overviewer/chunk.py
Normal file
505
overviewer/chunk.py
Normal file
@@ -0,0 +1,505 @@
|
||||
# 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, 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, 92])
|
||||
|
||||
# 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, 64, 65, 66, 67, 71, 73, 74, 78, 79, 80, 81, 82, 84, 86, 87, 88, 89, 91, 92])
|
||||
|
||||
# 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])
|
||||
|
||||
# chunkcoords should be the coordinates of a possible chunk. it may not exist
|
||||
def render_to_image(chunkcoords, img, imgcoords, quadtreeobj, cave=False, queue=None):
|
||||
"""Used to render a chunk to a tile in quadtree.py.
|
||||
|
||||
chunkcoords is a tuple: (chunkX, chunkY)
|
||||
|
||||
imgcoords is as well: (imgX, imgY), which represents the "origin"
|
||||
to use for drawing.
|
||||
|
||||
If the chunk doesn't exist, return False.
|
||||
Else, returns True."""
|
||||
a = ChunkRenderer(chunkcoords, quadtreeobj.world, quadtreeobj.rendermode, queue)
|
||||
try:
|
||||
a.chunk_render(img, imgcoords[0], imgcoords[1], cave)
|
||||
return True
|
||||
except ChunkCorrupt:
|
||||
# This should be non-fatal, but should print a warning
|
||||
pass
|
||||
except Exception, e:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
raise
|
||||
except KeyboardInterrupt:
|
||||
print
|
||||
print "You pressed Ctrl-C. Exiting..."
|
||||
# Raise an exception that is an instance of Exception. Unlike
|
||||
# KeyboardInterrupt, this will re-raise in the parent, killing the
|
||||
# entire program, instead of this process dying and the parent waiting
|
||||
# forever for it to finish.
|
||||
raise Exception()
|
||||
return False
|
||||
|
||||
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_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 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))
|
||||
red_color = Image.new("RGB", (24,24), (229,36,38))
|
||||
|
||||
# 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):
|
||||
img = Image.new("RGB", (24,24), (r,g,b))
|
||||
depth_colors.append(img)
|
||||
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()
|
||||
38
overviewer/composite.py
Normal file
38
overviewer/composite.py
Normal 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)
|
||||
168
overviewer/configParser.py
Normal file
168
overviewer/configParser.py
Normal file
@@ -0,0 +1,168 @@
|
||||
from optparse import OptionParser
|
||||
import sys
|
||||
import os.path
|
||||
|
||||
class OptionsResults(object):
|
||||
pass
|
||||
|
||||
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 = []
|
||||
|
||||
def display_config(self):
|
||||
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 os.path.exists(self.configFile):
|
||||
execfile(self.configFile, g, l)
|
||||
except NameError, ex:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
print "\nError 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
|
||||
print "\nError 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():
|
||||
print "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):
|
||||
raise Exception("%s is required" % n)
|
||||
|
||||
# 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:
|
||||
print "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']:
|
||||
print "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:
|
||||
print "Unknown type!"
|
||||
sys.exit(1)
|
||||
163
overviewer/googlemap.py
Normal file
163
overviewer/googlemap.py
Normal file
@@ -0,0 +1,163 @@
|
||||
# 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, gmtime
|
||||
import json
|
||||
|
||||
import util
|
||||
|
||||
"""
|
||||
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, skipjs=False, web_assets_hook=None):
|
||||
"""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 = skipjs
|
||||
self.web_assets_hook = web_assets_hook
|
||||
|
||||
if not len(quadtrees) > 0:
|
||||
raise ValueError("there must be at least one quadtree to work on")
|
||||
|
||||
self.destdir = quadtrees[0].destdir
|
||||
self.imgformat = quadtrees[0].imgformat
|
||||
self.world = quadtrees[0].world
|
||||
self.p = quadtrees[0].p
|
||||
for i in quadtrees:
|
||||
if i.destdir != self.destdir or i.imgformat != self.imgformat or i.world != self.world:
|
||||
raise ValueError("all the given quadtrees must have the same destdir")
|
||||
|
||||
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
|
||||
imgformat = self.imgformat
|
||||
configpath = os.path.join(util.get_program_path(), "config.js")
|
||||
|
||||
config = open(configpath, 'r').read()
|
||||
config = config.replace(
|
||||
"{maxzoom}", str(zoomlevel))
|
||||
config = config.replace(
|
||||
"{imgformat}", str(imgformat))
|
||||
|
||||
config = config.replace("{spawn_coords}",
|
||||
json.dumps(list(self.world.spawn)))
|
||||
|
||||
# create generated map type data, from given quadtrees
|
||||
maptypedata = map(lambda q: {'label' : q.rendermode.capitalize(),
|
||||
'path' : q.tiledir}, self.quadtrees)
|
||||
config = config.replace("{maptypedata}", json.dumps(maptypedata))
|
||||
|
||||
with open(os.path.join(self.destdir, "config.js"), 'w') as output:
|
||||
output.write(config)
|
||||
|
||||
# Write a blank image
|
||||
for quadtree in self.quadtrees:
|
||||
blank = Image.new("RGBA", (1,1))
|
||||
tileDir = os.path.join(self.destdir, quadtree.tiledir)
|
||||
if not os.path.exists(tileDir): os.mkdir(tileDir)
|
||||
blank.save(os.path.join(tileDir, "blank."+self.imgformat))
|
||||
|
||||
# copy web assets into destdir:
|
||||
mirror_dir(os.path.join(util.get_program_path(), "web_assets"), self.destdir)
|
||||
|
||||
# Add time 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 +0000", gmtime())))
|
||||
|
||||
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'])
|
||||
|
||||
# write out the default marker table
|
||||
with open(os.path.join(self.destdir, "markers.js"), 'w') as output:
|
||||
output.write("var markerData=%s" % json.dumps(self.world.POI))
|
||||
|
||||
# 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('var regionData=[\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/nbt.py
Normal file
404
overviewer/nbt.py
Normal 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)
|
||||
49
overviewer/optimizeimages.py
Normal file
49
overviewer/optimizeimages.py
Normal file
@@ -0,0 +1,49 @@
|
||||
# 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)
|
||||
|
||||
for prog,l in [(pngcrush,1), (optipng,2), (advdef,2)]:
|
||||
if l <= level:
|
||||
result = filter(lambda x: os.path.exists(os.path.join(x, prog)), path)
|
||||
if len(result) == 0:
|
||||
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:
|
||||
subprocess.Popen([optipng, imgpath], stderr=subprocess.STDOUT,
|
||||
stdout=subprocess.PIPE).communicate()[0]
|
||||
subprocess.Popen([advdef, "-z4",imgpath], stderr=subprocess.STDOUT,
|
||||
stdout=subprocess.PIPE).communicate()[0]
|
||||
|
||||
455
overviewer/quadtree.py
Normal file
455
overviewer/quadtree.py
Normal file
@@ -0,0 +1,455 @@
|
||||
# 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 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, depth=None, tiledir=None, imgformat=None, 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.imgformat = imgformat
|
||||
self.optimizeimg = optimizeimg
|
||||
|
||||
self.lighting = rendermode in ("lighting", "night", "spawn")
|
||||
self.night = rendermode in ("night", "spawn")
|
||||
self.spawn = rendermode in ("spawn",)
|
||||
self.rendermode = rendermode
|
||||
|
||||
# Make the destination dir
|
||||
if not os.path.exists(destdir):
|
||||
os.mkdir(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 '-z' or '--zoom' options.")
|
||||
|
||||
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, "config.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
|
||||
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
|
||||
|
||||
# return (col, row, chunkx, chunky, regionpath)
|
||||
chunkx, chunky = unconvert_coords(col, row)
|
||||
#c = get_region_path(chunkx, chunky)
|
||||
_, _, c, mcr = get_region((chunkx//32, chunky//32),(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), (38,92,255,0))
|
||||
|
||||
# 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=95, 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:
|
||||
# check region file mtime first.
|
||||
region,regionMtime = get_region_mtime(regionfile)
|
||||
if regionMtime <= tile_mtime:
|
||||
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), (38,92,255,0))
|
||||
|
||||
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!
|
||||
a = chunk.ChunkRenderer((chunkx, chunky), world, rendermode, poi_queue)
|
||||
a.chunk_render(tileimg, xpos, ypos, None)
|
||||
# chunk.render_to_image((chunkx, chunky), tileimg, (xpos, ypos), self, False, None)
|
||||
|
||||
|
||||
# Save them
|
||||
tileimg.save(imgpath)
|
||||
|
||||
if self.optimizeimg:
|
||||
optimize_image(imgpath, self.imgformat, self.optimizeimg)
|
||||
367
overviewer/rendernode.py
Normal file
367
overviewer/rendernode.py
Normal file
@@ -0,0 +1,367 @@
|
||||
# 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 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
|
||||
for quadtree in rendernode.quadtrees:
|
||||
if quadtree.world.useBiomeData:
|
||||
import textures
|
||||
# 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")
|
||||
|
||||
#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):
|
||||
"""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.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
|
||||
q.go(procs)
|
||||
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
|
||||
|
||||
356
overviewer/src/composite.c
Normal file
356
overviewer/src/composite.c
Normal file
@@ -0,0 +1,356 @@
|
||||
/*
|
||||
* 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,
|
||||
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++;
|
||||
} else if (*inmask == 0) {
|
||||
/* do nothing -- source is fully transparent */
|
||||
out += 3;
|
||||
} 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++;
|
||||
inmask += mask_stride;
|
||||
}
|
||||
}
|
||||
|
||||
return dest;
|
||||
}
|
||||
39
overviewer/src/endian.c
Normal file
39
overviewer/src/endian.c
Normal 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;
|
||||
}
|
||||
379
overviewer/src/iterate.c
Normal file
379
overviewer/src/iterate.c
Normal file
@@ -0,0 +1,379 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
int init_chunk_render(void) {
|
||||
|
||||
/* if blockmap (or any of these) is not NULL, then that means that we've
|
||||
* somehow called this function twice. error out so we can notice this
|
||||
* */
|
||||
if (blockmap) return 1;
|
||||
|
||||
textures = PyImport_ImportModule("overviewer.textures");
|
||||
/* ensure none of these pointers are NULL */
|
||||
if ((!textures)) {
|
||||
fprintf(stderr, "\ninit_chunk_render failed to load; textures\n");
|
||||
PyErr_Print();
|
||||
return 1;
|
||||
}
|
||||
|
||||
chunk_mod = PyImport_ImportModule("overviewer.chunk");
|
||||
/* ensure none of these pointers are NULL */
|
||||
if ((!chunk_mod)) {
|
||||
fprintf(stderr, "\ninit_chunk_render failed to load; chunk\n");
|
||||
PyErr_Print();
|
||||
return 1;
|
||||
}
|
||||
|
||||
blockmap = PyObject_GetAttrString(textures, "blockmap");
|
||||
special_blocks = PyObject_GetAttrString(textures, "special_blocks");
|
||||
specialblockmap = PyObject_GetAttrString(textures, "specialblockmap");
|
||||
transparent_blocks = PyObject_GetAttrString(chunk_mod, "transparent_blocks");
|
||||
|
||||
/* ensure none of these pointers are NULL */
|
||||
if ((!transparent_blocks) || (!blockmap) || (!special_blocks) || (!specialblockmap)) {
|
||||
fprintf(stderr, "\ninit_chunk_render failed\n");
|
||||
PyErr_Print();
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
||||
}
|
||||
|
||||
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 == 9) { /* water */
|
||||
/* an aditional bit for top is added to the 4 bits of check_adjacent_blocks */
|
||||
if ((ancilData == 0) || (ancilData >= 10)) { /* static water, only top, and unkown ancildata values */
|
||||
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) || (ancilData == 9)) { /* 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;
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
||||
}
|
||||
|
||||
|
||||
/* TODO triple check this to make sure reference counting is correct */
|
||||
PyObject*
|
||||
chunk_render(PyObject *self, PyObject *args) {
|
||||
RenderState state;
|
||||
|
||||
PyObject *blockdata_expanded;
|
||||
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, &blockdata_expanded))
|
||||
return Py_BuildValue("i", "-1");
|
||||
|
||||
/* 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;
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
/* get blockid */
|
||||
state.block = getArrayByte3D(blocks_py, state.x, state.y, state.z);
|
||||
if (state.block == 0) {
|
||||
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(blockdata_expanded, state.x, state.y, state.z);
|
||||
if ((state.block == 85) || (state.block == 9) || (state.block == 55)) {
|
||||
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;
|
||||
src = PyTuple_GetItem(t, 0);
|
||||
mask = PyTuple_GetItem(t, 1);
|
||||
|
||||
if (mask == Py_None)
|
||||
mask = src;
|
||||
|
||||
rendermode->draw(rm_data, &state, src, mask);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
46
overviewer/src/main.c
Normal file
46
overviewer/src/main.c
Normal file
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* 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 PyMethodDef COverviewerMethods[] = {
|
||||
{"alpha_over", alpha_over_wrap, METH_VARARGS,
|
||||
"alpha over composite function"},
|
||||
{"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"},
|
||||
{NULL, NULL, 0, NULL} /* Sentinel */
|
||||
};
|
||||
|
||||
PyMODINIT_FUNC
|
||||
initc_overviewer(void)
|
||||
{
|
||||
(void)Py_InitModule("c_overviewer", COverviewerMethods);
|
||||
/* for numpy */
|
||||
import_array();
|
||||
|
||||
/* initialize some required variables in iterage.c */
|
||||
if (init_chunk_render()) {
|
||||
fprintf(stderr, "failed to init_chunk_render\n");
|
||||
exit(1); // TODO better way to indicate error?
|
||||
}
|
||||
|
||||
init_endian();
|
||||
}
|
||||
86
overviewer/src/overviewer.h
Normal file
86
overviewer/src/overviewer.h
Normal file
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* 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__
|
||||
|
||||
/* 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,
|
||||
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 *blocks;
|
||||
PyObject *up_left_blocks;
|
||||
PyObject *up_right_blocks;
|
||||
PyObject *left_blocks;
|
||||
PyObject *right_blocks;
|
||||
} RenderState;
|
||||
int init_chunk_render(void);
|
||||
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__ */
|
||||
237
overviewer/src/rendermode-lighting.c
Normal file
237
overviewer/src/rendermode-lighting.c
Normal file
@@ -0,0 +1,237 @@
|
||||
/*
|
||||
* 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 float
|
||||
get_lighting_coefficient(RenderModeLighting *self, RenderState *state,
|
||||
int x, int y, int z, int *authoratative) {
|
||||
|
||||
/* 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;
|
||||
|
||||
/* 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;
|
||||
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);
|
||||
}
|
||||
|
||||
/* only do special half-step handling if no authoratative pointer was
|
||||
passed in, which is a sign that we're recursing */
|
||||
if (block == 44 && authoratative == NULL) {
|
||||
float average_gather = 0.0f;
|
||||
unsigned int average_count = 0;
|
||||
int auth;
|
||||
float coeff;
|
||||
|
||||
/* iterate through all surrounding blocks to take an average */
|
||||
int dx, dy, dz;
|
||||
for (dx = -1; dx <= 1; dx += 2) {
|
||||
for (dy = -1; dy <= 1; dy += 2) {
|
||||
for (dz = -1; dz <= 1; dz += 2) {
|
||||
coeff = get_lighting_coefficient(self, state, x+dx, y+dy, z+dz, &auth);
|
||||
if (auth) {
|
||||
average_gather += coeff;
|
||||
average_count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* only return the average if at least one was authoratative */
|
||||
if (average_count > 0)
|
||||
return average_gather / average_count;
|
||||
}
|
||||
|
||||
if (block == 10 || block == 11) {
|
||||
/* lava blocks should always be lit! */
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
skylevel = getArrayByte3D(skylight, local_x, local_y, local_z);
|
||||
blocklevel = getArrayByte3D(blocklight, local_x, local_y, local_z);
|
||||
|
||||
/* no longer a guess */
|
||||
if (authoratative)
|
||||
*authoratative = 1;
|
||||
|
||||
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, NULL);
|
||||
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) {
|
||||
RenderModeLighting* self;
|
||||
int x, y, z;
|
||||
|
||||
/* first, chain up */
|
||||
rendermode_normal.draw(data, state, src, mask);
|
||||
|
||||
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);
|
||||
} 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",
|
||||
sizeof(RenderModeLighting),
|
||||
rendermode_lighting_start,
|
||||
rendermode_lighting_finish,
|
||||
rendermode_lighting_occluded,
|
||||
rendermode_lighting_draw,
|
||||
};
|
||||
69
overviewer/src/rendermode-night.c
Normal file
69
overviewer/src/rendermode-night.c
Normal file
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* 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) {
|
||||
/* nothing special to do */
|
||||
rendermode_lighting.draw(data, state, src, mask);
|
||||
}
|
||||
|
||||
RenderModeInterface rendermode_night = {
|
||||
"night", "like \"lighting\", except at night",
|
||||
sizeof(RenderModeNight),
|
||||
rendermode_night_start,
|
||||
rendermode_night_finish,
|
||||
rendermode_night_occluded,
|
||||
rendermode_night_draw,
|
||||
};
|
||||
173
overviewer/src/rendermode-normal.c
Normal file
173
overviewer/src/rendermode-normal.c
Normal file
@@ -0,0 +1,173 @@
|
||||
/*
|
||||
* 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);
|
||||
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");
|
||||
|
||||
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->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->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) {
|
||||
RenderModeNormal *self = (RenderModeNormal *)data;
|
||||
|
||||
/* first, check to see if we should use biome-compatible src, mask */
|
||||
if (self->biome_data) {
|
||||
switch (state->block) {
|
||||
case 2:
|
||||
src = mask = self->grass_texture;
|
||||
break;
|
||||
case 18:
|
||||
src = mask = self->leaf_texture;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
};
|
||||
}
|
||||
|
||||
/* 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 */
|
||||
color = PySequence_GetItem(self->grasscolor, index);
|
||||
facemask = self->facemask_top;
|
||||
break;
|
||||
case 18:
|
||||
/* leaves */
|
||||
color = PySequence_GetItem(self->foliagecolor, index);
|
||||
facemask = mask;
|
||||
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, facemask, state->imgx, state->imgy, 0, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RenderModeInterface rendermode_normal = {
|
||||
"normal", "nothing special, just render the blocks",
|
||||
sizeof(RenderModeNormal),
|
||||
rendermode_normal_start,
|
||||
rendermode_normal_finish,
|
||||
rendermode_normal_occluded,
|
||||
rendermode_normal_draw,
|
||||
};
|
||||
127
overviewer/src/rendermode-spawn.c
Normal file
127
overviewer/src/rendermode-spawn.c
Normal file
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
* 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 int
|
||||
rendermode_spawn_start(void *data, RenderState *state) {
|
||||
RenderModeSpawn* self;
|
||||
|
||||
/* first, chain up */
|
||||
int ret = rendermode_night.start(data, state);
|
||||
if (ret != 0)
|
||||
return ret;
|
||||
|
||||
/* now do custom initializations */
|
||||
self = (RenderModeSpawn *)data;
|
||||
self->solid_blocks = PyObject_GetAttrString(state->chunk, "solid_blocks");
|
||||
self->nospawn_blocks = PyObject_GetAttrString(state->chunk, "nospawn_blocks");
|
||||
self->fluid_blocks = PyObject_GetAttrString(state->chunk, "fluid_blocks");
|
||||
self->red_color = PyObject_GetAttrString(state->chunk, "red_color");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void
|
||||
rendermode_spawn_finish(void *data, RenderState *state) {
|
||||
/* first free all *our* stuff */
|
||||
RenderModeSpawn* self = (RenderModeSpawn *)data;
|
||||
|
||||
Py_DECREF(self->solid_blocks);
|
||||
Py_DECREF(self->nospawn_blocks);
|
||||
Py_DECREF(self->fluid_blocks);
|
||||
|
||||
/* now, chain up */
|
||||
rendermode_night.finish(data, state);
|
||||
}
|
||||
|
||||
static int
|
||||
rendermode_spawn_occluded(void *data, RenderState *state) {
|
||||
/* no special occlusion here */
|
||||
return rendermode_night.occluded(data, state);
|
||||
}
|
||||
|
||||
static void
|
||||
rendermode_spawn_draw(void *data, RenderState *state, PyObject *src, PyObject *mask) {
|
||||
/* different versions of self (spawn, lighting) */
|
||||
RenderModeSpawn* self = (RenderModeSpawn *)data;
|
||||
RenderModeLighting *lighting = (RenderModeLighting *)self;
|
||||
|
||||
int x = state->x, y = state->y, z = state->z;
|
||||
PyObject *old_black_color = NULL;
|
||||
|
||||
/* figure out the appropriate darkness:
|
||||
this block for transparents, the block above for non-transparent */
|
||||
float darkness = 0.0;
|
||||
if (is_transparent(state->block)) {
|
||||
darkness = get_lighting_coefficient((RenderModeLighting *)self, state, x, y, z, NULL);
|
||||
} else {
|
||||
darkness = get_lighting_coefficient((RenderModeLighting *)self, state, x, y, z+1, NULL);
|
||||
}
|
||||
|
||||
/* if it's dark enough... */
|
||||
if (darkness > 0.8) {
|
||||
PyObject *block_py = PyInt_FromLong(state->block);
|
||||
|
||||
/* make sure it's solid */
|
||||
if (PySequence_Contains(self->solid_blocks, block_py)) {
|
||||
int spawnable = 1;
|
||||
|
||||
/* not spawnable if its in the nospawn list */
|
||||
if (PySequence_Contains(self->nospawn_blocks, block_py))
|
||||
spawnable = 0;
|
||||
|
||||
/* check the block above for solid or fluid */
|
||||
if (spawnable && z != 127) {
|
||||
PyObject *top_block_py = PyInt_FromLong(getArrayByte3D(state->blocks, x, y, z+1));
|
||||
if (PySequence_Contains(self->solid_blocks, top_block_py) ||
|
||||
PySequence_Contains(self->fluid_blocks, top_block_py)) {
|
||||
|
||||
spawnable = 0;
|
||||
}
|
||||
|
||||
Py_DECREF(top_block_py);
|
||||
}
|
||||
|
||||
/* if we passed all the checks, replace black_color with red_color */
|
||||
if (spawnable) {
|
||||
old_black_color = lighting->black_color;
|
||||
lighting->black_color = self->red_color;
|
||||
}
|
||||
}
|
||||
|
||||
Py_DECREF(block_py);
|
||||
}
|
||||
|
||||
/* draw normally */
|
||||
rendermode_night.draw(data, state, src, mask);
|
||||
|
||||
/* reset black_color, if needed */
|
||||
if (old_black_color != NULL) {
|
||||
lighting->black_color = old_black_color;
|
||||
}
|
||||
}
|
||||
|
||||
RenderModeInterface rendermode_spawn = {
|
||||
"spawn", "draws red where monsters can spawn at night",
|
||||
sizeof(RenderModeSpawn),
|
||||
rendermode_spawn_start,
|
||||
rendermode_spawn_finish,
|
||||
rendermode_spawn_occluded,
|
||||
rendermode_spawn_draw,
|
||||
};
|
||||
101
overviewer/src/rendermodes.c
Normal file
101
overviewer/src/rendermodes.c
Normal file
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* 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,
|
||||
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);
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
122
overviewer/src/rendermodes.h
Normal file
122
overviewer/src/rendermodes.h
Normal file
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* 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 {
|
||||
/* the name of this mode */
|
||||
const char* name;
|
||||
/* the short description of this render mode */
|
||||
const char* description;
|
||||
|
||||
/* 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 *);
|
||||
} RenderModeInterface;
|
||||
|
||||
/* 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);
|
||||
|
||||
/* 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;
|
||||
/* top facemask for grass biome tinting */
|
||||
PyObject *facemask_top;
|
||||
} RenderModeNormal;
|
||||
extern RenderModeInterface rendermode_normal;
|
||||
|
||||
/* 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, int *authoratative);
|
||||
|
||||
/* NIGHT */
|
||||
typedef struct {
|
||||
/* inherits from lighting */
|
||||
RenderModeLighting parent;
|
||||
} RenderModeNight;
|
||||
extern RenderModeInterface rendermode_night;
|
||||
|
||||
/* SPAWN */
|
||||
typedef struct {
|
||||
/* inherits from night */
|
||||
RenderModeNight parent;
|
||||
|
||||
/* used to figure out which blocks are spawnable */
|
||||
PyObject *solid_blocks, *nospawn_blocks, *fluid_blocks;
|
||||
/* replacement for black_color */
|
||||
PyObject *red_color;
|
||||
} RenderModeSpawn;
|
||||
extern RenderModeInterface rendermode_spawn;
|
||||
|
||||
#endif /* __RENDERMODES_H_INCLUDED__ */
|
||||
1122
overviewer/textures.py
Normal file
1122
overviewer/textures.py
Normal file
File diff suppressed because it is too large
Load Diff
51
overviewer/util.py
Normal file
51
overviewer/util.py
Normal file
@@ -0,0 +1,51 @@
|
||||
# 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
|
||||
|
||||
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/util.py
|
||||
return os.path.dirname(os.path.dirname(__file__))
|
||||
except NameError:
|
||||
return os.path.dirname(sys.argv[0])
|
||||
|
||||
|
||||
|
||||
def findGitVersion():
|
||||
if os.path.exists(".git"):
|
||||
with open(os.path.join(".git","HEAD")) as f:
|
||||
data = f.read().strip()
|
||||
if data.startswith("ref: "):
|
||||
with open(os.path.join(".git", data[5:])) as g:
|
||||
return g.read().strip()
|
||||
else:
|
||||
return data
|
||||
else:
|
||||
try:
|
||||
import overviewer_version
|
||||
return overviewer_version.VERSION
|
||||
except:
|
||||
return "unknown"
|
||||
340
overviewer/world.py
Normal file
340
overviewer/world.py
Normal file
@@ -0,0 +1,340 @@
|
||||
# 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
|
||||
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 = {}
|
||||
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)
|
||||
|
||||
if self.useBiomeData:
|
||||
textures.prepareBiomeData(worlddir)
|
||||
|
||||
# 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)
|
||||
|
||||
data=nbt.load_from_region(chunkFile, chunkX, chunkY)[1]
|
||||
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=(inChunkX,inChunkZ)))
|
||||
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
|
||||
|
||||
Returns (regionx, regiony, filename)"""
|
||||
join = os.path.join
|
||||
if regionlist is not None:
|
||||
for path in regionlist:
|
||||
if path.endswith("\n"):
|
||||
path = path[:-1]
|
||||
f = os.path.basename(path)
|
||||
if f.startswith("r.") and f.endswith(".mcr"):
|
||||
p = f.split(".")
|
||||
yield (int(p[1]), int(p[2]), join(self.worlddir, 'region', f))
|
||||
else:
|
||||
for dirpath, dirnames, filenames in os.walk(os.path.join(self.worlddir, 'region')):
|
||||
if not dirnames and filenames and "DIM-1" not in dirpath:
|
||||
for f in filenames:
|
||||
if f.startswith("r.") and f.endswith(".mcr"):
|
||||
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
|
||||
Reference in New Issue
Block a user