0

Merge remote branch 'eminence/master'

This commit is contained in:
Andrew Brown
2010-11-07 09:08:56 -05:00
12 changed files with 294 additions and 23 deletions

View File

@@ -86,6 +86,11 @@ def get_blockdata_array(level):
in a similar manner to skylight data""" in a similar manner to skylight data"""
return numpy.frombuffer(level['Data'], dtype=numpy.uint8).reshape((16,16,64)) 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
def iterate_chunkblocks(xoff,yoff): def iterate_chunkblocks(xoff,yoff):
"""Iterates over the 16x16x128 blocks of a chunk in rendering order. """Iterates over the 16x16x128 blocks of a chunk in rendering order.
Yields (x,y,z,imgx,imgy) Yields (x,y,z,imgx,imgy)
@@ -105,12 +110,12 @@ def iterate_chunkblocks(xoff,yoff):
transparent_blocks = set([0, 6, 8, 9, 18, 20, 37, 38, 39, 40, 44, 50, 51, 52, 53, transparent_blocks = set([0, 6, 8, 9, 18, 20, 37, 38, 39, 40, 44, 50, 51, 52, 53,
59, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 74, 75, 76, 77, 78, 79, 81, 83, 85]) 59, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 74, 75, 76, 77, 78, 79, 81, 83, 85])
def render_and_save(chunkfile, cachedir, worldobj, cave=False): def render_and_save(chunkfile, cachedir, worldobj, cave=False, queue=None):
"""Used as the entry point for the multiprocessing workers (since processes """Used as the entry point for the multiprocessing workers (since processes
can't target bound methods) or to easily render and save one chunk can't target bound methods) or to easily render and save one chunk
Returns the image file location""" Returns the image file location"""
a = ChunkRenderer(chunkfile, cachedir, worldobj) a = ChunkRenderer(chunkfile, cachedir, worldobj, queue)
try: try:
return a.render_and_save(cave) return a.render_and_save(cave)
except ChunkCorrupt: except ChunkCorrupt:
@@ -133,21 +138,29 @@ class ChunkCorrupt(Exception):
pass pass
class ChunkRenderer(object): class ChunkRenderer(object):
def __init__(self, chunkfile, cachedir, worldobj): def __init__(self, chunkfile, cachedir, worldobj, queue):
"""Make a new chunk renderer for the given chunkfile. """Make a new chunk renderer for the given chunkfile.
chunkfile should be a full path to the .dat file to process chunkfile should be a full path to the .dat file to process
cachedir is a directory to save the resulting chunk images to cachedir is a directory to save the resulting chunk images to
""" """
self.queue = queue
if not os.path.exists(chunkfile): if not os.path.exists(chunkfile):
raise ValueError("Could not find chunkfile") raise ValueError("Could not find chunkfile")
self.chunkfile = chunkfile self.chunkfile = chunkfile
destdir, filename = os.path.split(self.chunkfile) destdir, filename = os.path.split(self.chunkfile)
filename_split = filename.split(".")
chunkcoords = filename_split[1:3]
chunkcoords = filename.split(".")[1:3]
self.coords = map(world.base36decode, chunkcoords) self.coords = map(world.base36decode, chunkcoords)
self.blockid = ".".join(chunkcoords) self.blockid = ".".join(chunkcoords)
self.world = worldobj
# chunk coordinates (useful to converting local block coords to
# global block coords)
self.chunkX = int(filename_split[1], base=36)
self.chunkY = int(filename_split[2], base=36)
self.world = worldobj
# Cachedir here is the base directory of the caches. We need to go 2 # Cachedir here is the base directory of the caches. We need to go 2
# levels deeper according to the chunk file. Get the last 2 components # levels deeper according to the chunk file. Get the last 2 components
# of destdir and use that # of destdir and use that
@@ -298,7 +311,7 @@ class ChunkRenderer(object):
is up to date, this method doesn't render anything. is up to date, this method doesn't render anything.
""" """
blockid = self.blockid blockid = self.blockid
oldimg, oldimg_path = self.find_oldimage(cave) oldimg, oldimg_path = self.find_oldimage(cave)
if oldimg: if oldimg:
@@ -479,6 +492,8 @@ class ChunkRenderer(object):
# Odd elements get the upper 4 bits # Odd elements get the upper 4 bits
blockData_expanded[:,:,1::2] = blockData >> 4 blockData_expanded[:,:,1::2] = blockData >> 4
tileEntities = get_tileentity_data(self.level)
# Each block is 24x24 # 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 X axis adds 12px to x and subtracts 6px from y in the image
@@ -509,6 +524,7 @@ class ChunkRenderer(object):
else: else:
t = textures.blockmap[blockid] t = textures.blockmap[blockid]
if not t: if not t:
continue continue
@@ -607,6 +623,30 @@ class ChunkRenderer(object):
if y != 0 and blocks[x,y-1,z] == 0: if y != 0 and blocks[x,y-1,z] == 0:
draw.line(((imgx,imgy+6+increment), (imgx+12,imgy+increment)), fill=(0,0,0), width=1) draw.line(((imgx,imgy+6+increment), (imgx+12,imgy+increment)), fill=(0,0,0), width=1)
for entity in tileEntities:
if entity['id'] == 'Sign':
# 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="%s\n%s\n%s\n%s" %
(entity['Text1'], entity['Text2'], entity['Text3'], entity['Text4']),
chunk= (self.chunkX, self.chunkY),
)
self.queue.put(["newpoi", newPOI])
# check to see if there are any signs in the persistentData list that are from this chunk.
# if so, remove them from the persistentData list (since they're have been added to the world.POI
# list above.
self.queue.put(['removePOI', (self.chunkX, self.chunkY)])
return img return img
# Render 3 blending masks for lighting # Render 3 blending masks for lighting

64
contrib/findSigns.py Normal file
View File

@@ -0,0 +1,64 @@
#!/usr/bin/python
'''
This script will scan through every chunk looking for signs and write out an
updated overviewer.dat file. This can be useful if your overviewer.dat file
is either out-of-date or non-existant.
To run, simply give a path to your world directory, for example:
python contrib/findSigns.py ../world.test/
Once that is done, simply re-run the overviewer to generate markers.js:
python gmap.py ../world.test/ output_dir/
Note: if your cachedir is not the same as your world-dir, you'll need to manually
move overviewer.dat into the correct location.
'''
import sys
import re
import os
import cPickle
sys.path.append(".")
import nbt
from pprint import pprint
worlddir = sys.argv[1]
if os.path.exists(worlddir):
print "Scanning chunks in ", worlddir
else:
sys.exit("Bad WorldDir")
matcher = re.compile(r"^c\..*\.dat$")
POI = []
for dirpath, dirnames, filenames in os.walk(worlddir):
for f in filenames:
if matcher.match(f):
full = os.path.join(dirpath, f)
#print "inspecting %s" % full
data = nbt.load(full)[1]['Level']['TileEntities']
for entity in data:
if entity['id'] == 'Sign':
newPOI = dict(type="sign",
x= entity['x'],
y= entity['y'],
z= entity['z'],
msg="%s\n%s\n%s\n%s" %
(entity['Text1'], entity['Text2'], entity['Text3'], entity['Text4']),
chunk= (entity['x']/16, entity['z']/16),
)
POI.append(newPOI)
print "Found sign at (%d, %d, %d): %r" % (newPOI['x'], newPOI['y'], newPOI['z'], newPOI['msg'])
pickleFile = os.path.join(worlddir,"overviewer.dat")
with open(pickleFile,"wb") as f:
cPickle.dump(dict(POI=POI), f)

View File

@@ -145,6 +145,12 @@ def delete_all(worlddir, tiledir):
logging.info("Deleting {0}".format(filepath)) logging.info("Deleting {0}".format(filepath))
os.unlink(filepath) os.unlink(filepath)
# delete the overviewer.dat persistant data file
datfile = os.path.join(worlddir,"overviewer.dat")
if os.path.exists(datfile):
os.unlink(datfile)
logging.info("Deleting {0}".format(datfile))
def list_worlds(): def list_worlds():
"Prints out a brief summary of saves found in the default directory" "Prints out a brief summary of saves found in the default directory"
print print

View File

@@ -25,6 +25,7 @@ import collections
import json import json
import logging import logging
import util import util
import cPickle
from PIL import Image from PIL import Image
@@ -144,12 +145,29 @@ class QuadtreeGen(object):
if not os.path.exists(tileDir): os.mkdir(tileDir) if not os.path.exists(tileDir): os.mkdir(tileDir)
blank.save(os.path.join(tileDir, "blank."+self.imgformat)) blank.save(os.path.join(tileDir, "blank."+self.imgformat))
# copy web assets into destdir:
for root, dirs, files in os.walk(os.path.join(util.get_program_path(), "web_assets")):
for f in files:
shutil.copy(os.path.join(root, f), self.destdir)
if skipjs: if skipjs:
return 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 # write out the default marker table
with open(os.path.join(self.destdir, "markers.js"), 'w') as output: with open(os.path.join(self.destdir, "markers.js"), 'w') as output:
output.write("var markerData=%s" % json.dumps(self.world.POI)) 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 # write out the default (empty, but documented) region table
with open(os.path.join(self.destdir, "regions.js"), 'w') as output: with open(os.path.join(self.destdir, "regions.js"), 'w') as output:

View File

@@ -2,11 +2,7 @@
<html> <html>
<head> <head>
<meta name="viewport" content="initial-scale=1.0, user-scalable=no" /> <meta name="viewport" content="initial-scale=1.0, user-scalable=no" />
<style type="text/css"> <link rel="stylesheet" href="style.css" type="text/css" />
html { height: 100% }
body { height: 100%; margin: 0px; padding: 0px ; background-color: #000; }
#mcmap { height: 100% }
</style>
<script type="text/javascript" src="markers.js"></script> <script type="text/javascript" src="markers.js"></script>
<script type="text/javascript" src="regions.js"></script> <script type="text/javascript" src="regions.js"></script>
<script type="text/javascript" <script type="text/javascript"
@@ -138,7 +134,19 @@
var map; var map;
var markersInit = false; var markersInit = false;
function prepareSignMarker(marker, item) {
var c = "<div class=\"infoWindow\"><img src=\"signpost.png\" /><p>" + item.msg.replace(/\n/g,"<br/>") + "</p></div>";
var infowindow = new google.maps.InfoWindow({
content: c
});
google.maps.event.addListener(marker, 'click', function() {
infowindow.open(map,marker);
});
}
function initMarkers() { function initMarkers() {
if (markersInit) { return; } if (markersInit) { return; }
@@ -146,13 +154,26 @@
for (i in markerData) { for (i in markerData) {
var item = markerData[i]; var item = markerData[i];
// a default:
var iconURL = '';
if (item.type == 'spawn') { iconURL = 'http://google-maps-icons.googlecode.com/files/home.png';}
if (item.type == 'sign') { iconURL = 'signpost_icon.png';}
var converted = fromWorldToLatLng(item.x, item.y, item.z); var converted = fromWorldToLatLng(item.x, item.y, item.z);
var marker = new google.maps.Marker({ var marker = new google.maps.Marker({
position: converted, position: converted,
map: map, map: map,
title: item.msg title: item.msg,
}); icon: iconURL
});
if (item.type == 'sign') {
prepareSignMarker(marker, item);
}
} }
} }
@@ -234,6 +255,18 @@
// initialize the markers and regions // initialize the markers and regions
initMarkers(); initMarkers();
initRegions(); initRegions();
var compassDiv = document.createElement('DIV');
compassDiv.style.padding = '5px';
var compassImg = document.createElement('IMG');
compassImg.src="compass.png";
compassDiv.appendChild(compassImg);
map.controls[google.maps.ControlPosition.TOP_RIGHT].push(compassDiv);
} }
</script> </script>
</head> </head>

View File

@@ -21,7 +21,7 @@ from cStringIO import StringIO
import math import math
import numpy import numpy
from PIL import Image, ImageEnhance from PIL import Image, ImageEnhance, ImageOps
import util import util
import composite import composite
@@ -268,8 +268,8 @@ def _build_blockimages():
36, 37, 80, -1, 65, 4, 25,101, 98, 24, 43, -1, 86, 1, 1, -1, # Torch from above? leaving out fire. Redstone wire? Crops/furnaces handled elsewhere. sign post 36, 37, 80, -1, 65, 4, 25,101, 98, 24, 43, -1, 86, 1, 1, -1, # Torch from above? leaving out fire. Redstone wire? Crops/furnaces handled elsewhere. sign post
# 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 # 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
-1, -1, -1, 16, -1, -1, -1, -1, -1, 51, 51, -1, -1, 1, 66, 67, # door,ladder left out. Minecart rail orientation -1, -1, -1, 16, -1, -1, -1, -1, -1, 51, 51, -1, -1, 1, 66, 67, # door,ladder left out. Minecart rail orientation
# 80 81 82 83 84 # 80 81 82 83 84 85 86 87 88 89 90 91
66, 69, 72, 73, 74 # clay? 66, 69, 72, 73, 74, -1,102,103,104,105,-1, 102 # clay?
] ]
# NOTE: For non-block textures, the sideid is ignored, but can't be -1 # NOTE: For non-block textures, the sideid is ignored, but can't be -1
@@ -285,8 +285,8 @@ def _build_blockimages():
36, 37, 80, -1, 65, 4, 25,101, 98, 24, 43, -1, 86, 44, 61, -1, 36, 37, 80, -1, 65, 4, 25,101, 98, 24, 43, -1, 86, 44, 61, -1,
# 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 # 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
-1, -1, -1, 16, -1, -1, -1, -1, -1, 51, 51, -1, -1, 1, 66, 67, -1, -1, -1, 16, -1, -1, -1, -1, -1, 51, 51, -1, -1, 1, 66, 67,
# 80 81 82 83 84 # 80 81 82 83 84 85 86 87 88 89 90 91
66, 69, 72, 73, 74 66, 69, 72, 73, 74,-1 ,118,103,104,105, -1, 118
] ]
# This maps block id to the texture that goes on the side of the block # This maps block id to the texture that goes on the side of the block
@@ -393,6 +393,19 @@ def generate_special_texture(blockID, data):
composite.alpha_over(img, side2, (12,6), side2) composite.alpha_over(img, side2, (12,6), side2)
composite.alpha_over(img, top, (0,0), top) composite.alpha_over(img, top, (0,0), top)
return (img.convert("RGB"), img.split()[3]) return (img.convert("RGB"), img.split()[3])
if blockID in (86,91): # jack-o-lantern
top = transform_image(terrain_images[102])
frontID = 119 if blockID == 86 else 120
side1 = transform_image_side(terrain_images[frontID])
side2 = transform_image_side(terrain_images[118]).transpose(Image.FLIP_LEFT_RIGHT)
img = Image.new("RGBA", (24,24), (38,92,255,0))
img.paste(side1, (0,6), side1)
img.paste(side2, (12,6), side2)
img.paste(top, (0,0), top)
return (img.convert("RGB"), img.split()[3])
if blockID == 62: # lit furnace if blockID == 62: # lit furnace
top = transform_image(terrain_images[1]) top = transform_image(terrain_images[1])
@@ -484,13 +497,43 @@ def generate_special_texture(blockID, data):
return (img.convert("RGB"), img.split()[3]) return (img.convert("RGB"), img.split()[3])
if blockID == 2: # grass
top = transform_image(tintTexture(terrain_images[0],(170,255,50)))
side1 = transform_image_side(terrain_images[3])
side2 = transform_image_side(terrain_images[3]).transpose(Image.FLIP_LEFT_RIGHT)
img = Image.new("RGBA", (24,24), (38,92,255,0))
img.paste(side1, (0,6), side1)
img.paste(side2, (12,6), side2)
img.paste(top, (0,0), top)
return (img.convert("RGB"), img.split()[3])
if blockID == 18: # leaves
t = tintTexture(terrain_images[52], (170, 255, 50))
top = transform_image(t)
side1 = transform_image_side(t)
side2 = transform_image_side(t).transpose(Image.FLIP_LEFT_RIGHT)
img = Image.new("RGBA", (24,24), (38,92,255,0))
img.paste(side1, (0,6), side1)
img.paste(side2, (12,6), side2)
img.paste(top, (0,0), top)
return (img.convert("RGB"), img.split()[3])
return None return None
def tintTexture(im, c):
# apparently converting to grayscale drops the alpha channel?
i = ImageOps.colorize(ImageOps.grayscale(im), (0,0,0), c)
i.putalpha(im.split()[3]); # copy the alpha band back in. assuming RGBA
return i
# This set holds block ids that require special pre-computing. These are typically # This set holds block ids that require special pre-computing. These are typically
# things that require ancillary data to render properly (i.e. ladder plus orientation) # things that require ancillary data to render properly (i.e. ladder plus orientation)
special_blocks = set([66,59,61,62, 65,64,71]) special_blocks = set([66,59,61,62, 65,64,71,91,86,2,18])
# this is a map of special blockIDs to a list of all # this is a map of special blockIDs to a list of all
# possible values for ancillary data that it might have. # possible values for ancillary data that it might have.
@@ -502,6 +545,18 @@ special_map[62] = (0,) # burning furnace
special_map[65] = (2,3,4,5) # ladder special_map[65] = (2,3,4,5) # ladder
special_map[64] = range(16) # wooden door special_map[64] = range(16) # wooden door
special_map[71] = range(16) # iron door special_map[71] = range(16) # iron door
special_map[91] = range(5) # jack-o-lantern
special_map[86] = range(5) # pumpkin
# apparently pumpkins and jack-o-lanterns have ancillary data, but it's unknown
# what that data represents. For now, assume that the range for data is 0 to 5
# like torches
special_map[2] = (0,) # grass
special_map[18] = range(16) # leaves
# grass and leaves are now graysacle in terrain.png
# we treat them as special so we can manually tint them
# it is unknown how the specific tint (biomes) is calculated
# leaves have ancilary data, but its meaning is unknown (age perhaps?)
specialblockmap = {} specialblockmap = {}

BIN
web_assets/compass.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 B

BIN
web_assets/signpost.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 708 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 253 B

18
web_assets/style.css Normal file
View File

@@ -0,0 +1,18 @@
html { height: 100% }
body { height: 100%; margin: 0px; padding: 0px ; background-color: #000; }
#mcmap { height: 100% }
.infoWindow {
height: 100px;
}
.infoWindow>img {
width:80px;
float: left;
}
.infoWindow>p {
text-align: center;
font-family: monospace;
}

View File

@@ -17,8 +17,10 @@ import functools
import os import os
import os.path import os.path
import multiprocessing import multiprocessing
import Queue
import sys import sys
import logging import logging
import cPickle
import numpy import numpy
@@ -105,6 +107,20 @@ class WorldRenderer(object):
# a list of dictionaries, see below for an example # a list of dictionaries, see below for an example
self.POI = [] 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)
self.pickleFile = os.path.join(self.cachedir,"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_chunk_renderset(self): def _get_chunk_renderset(self):
"""Returns a set of (col, row) chunks that should be rendered. Returns """Returns a set of (col, row) chunks that should be rendered. Returns
None if all chunks should be rendered""" None if all chunks should be rendered"""
@@ -180,7 +196,8 @@ class WorldRenderer(object):
spawnY += 1 spawnY += 1
self.POI.append( dict(x=spawnX, y=spawnY, z=spawnZ, msg="Spawn")) self.POI.append( dict(x=spawnX, y=spawnY, z=spawnZ,
msg="Spawn", type="spawn", chunk=(inChunkX,inChunkZ)))
def go(self, procs): def go(self, procs):
"""Starts the render. This returns when it is finished""" """Starts the render. This returns when it is finished"""
@@ -242,6 +259,9 @@ class WorldRenderer(object):
inclusion_set = self._get_chunk_renderset() inclusion_set = self._get_chunk_renderset()
results = {} results = {}
manager = multiprocessing.Manager()
q = manager.Queue()
if processes == 1: if processes == 1:
# Skip the multiprocessing stuff # Skip the multiprocessing stuff
logging.debug("Rendering chunks synchronously since you requested 1 process") logging.debug("Rendering chunks synchronously since you requested 1 process")
@@ -254,9 +274,17 @@ class WorldRenderer(object):
results[(col, row)] = imgpath results[(col, row)] = imgpath
continue continue
result = chunk.render_and_save(chunkfile, self.cachedir, self, cave=self.caves) result = chunk.render_and_save(chunkfile, self.cachedir, self, cave=self.caves, queue=q)
results[(col, row)] = result results[(col, row)] = result
if i > 0: if i > 0:
try:
item = q.get(block=False)
if item[0] == "newpoi":
self.POI.append(item[1])
elif item[0] == "removePOI":
self.persistentData['POI'] = filter(lambda x: x['chunk'] != item[1], self.persistentData['POI'])
except Queue.Empty:
pass
if 1000 % i == 0 or i % 1000 == 0: if 1000 % i == 0 or i % 1000 == 0:
logging.info("{0}/{1} chunks rendered".format(i, len(chunks))) logging.info("{0}/{1} chunks rendered".format(i, len(chunks)))
else: else:
@@ -274,13 +302,22 @@ class WorldRenderer(object):
result = pool.apply_async(chunk.render_and_save, result = pool.apply_async(chunk.render_and_save,
args=(chunkfile,self.cachedir,self), args=(chunkfile,self.cachedir,self),
kwds=dict(cave=self.caves)) kwds=dict(cave=self.caves, queue=q))
asyncresults.append((col, row, result)) asyncresults.append((col, row, result))
pool.close() pool.close()
for i, (col, row, result) in enumerate(asyncresults): for i, (col, row, result) in enumerate(asyncresults):
results[(col, row)] = result.get() results[(col, row)] = result.get()
try:
item = q.get(block=False)
if item[0] == "newpoi":
self.POI.append(item[1])
elif item[0] == "removePOI":
self.persistentData['POI'] = filter(lambda x: x['chunk'] != item[1], self.persistentData['POI'])
except Queue.Empty:
pass
if i > 0: if i > 0:
if 1000 % i == 0 or i % 1000 == 0: if 1000 % i == 0 or i % 1000 == 0:
logging.info("{0}/{1} chunks rendered".format(i, len(asyncresults))) logging.info("{0}/{1} chunks rendered".format(i, len(asyncresults)))