#!/usr/bin/env python2
# -*- coding: utf-8 -*-
# minetestmapper-numpy.py is free software. It comes without any warranty, to
# the extent permitted by applicable law. You can redistribute it
# and/or modify it under the terms of minetestmapper-numpy LICENSE available here: https://github.com/spillz/minetest/blob/master/util/minetestmapper-numpy.py
# Made by Jogge, modified by celeron55
# 2011-05-29: j0gge: initial release
# 2011-05-30: celeron55: simultaneous support for sectors/sectors2, removed
# 2011-06-02: j0gge: command line parameters, coordinates, players, ...
# 2011-06-04: celeron55: added #!/usr/bin/python2 and converted \r\n to \n
# to make it easily executable on Linux
# 2011-07-30: WF: Support for content types extension, refactoring
# 2011-07-30: erlehmann: PEP 8 compliance.
# 2014-03-05: spillz: Refactored code, use argparse for better command line handling,
# use numpy for speed boost and reduced memory usage
# Requires Python Imaging Library: http://www.pythonware.com/products/pil/
# Requires Numpy: http://www.scipy.org
import zlib
import os
import string
import time
import argparse
import sys
import traceback
import numpy
import itertools
from PIL import Image , ImageDraw , ImageFont , ImageColor
try :
import io
BytesIO = io . BytesIO
except :
import cStringIO
BytesIO = cStringIO . StringIO
#
# wrapper around PIL 1.1.6 Image.save to preserve PNG metadata
#
# public domain, Nick Galbreath
# http://blog.client9.com/2007/08/28/python-pil-and-png-metadata-take-2.html
#
def pngsave ( im , file ) :
# these can be automatically added to Image.info dict
# they are not user-added metadata
reserved = ( ' interlace ' , ' gamma ' , ' dpi ' , ' transparency ' , ' aspect ' )
# undocumented class
from PIL import PngImagePlugin
meta = PngImagePlugin . PngInfo ( )
# copy metadata into new object
for k , v in im . info . iteritems ( ) :
if k in reserved : continue
meta . add_text ( k , v , 0 )
# and save
im . save ( file , " PNG " , pnginfo = meta )
TRANSLATION_TABLE = {
1 : 0x800 , # CONTENT_GRASS
4 : 0x801 , # CONTENT_TREE
5 : 0x802 , # CONTENT_LEAVES
6 : 0x803 , # CONTENT_GRASS_FOOTSTEPS
7 : 0x804 , # CONTENT_MESE
8 : 0x805 , # CONTENT_MUD
10 : 0x806 , # CONTENT_CLOUD
11 : 0x807 , # CONTENT_COALSTONE
12 : 0x808 , # CONTENT_WOOD
13 : 0x809 , # CONTENT_SAND
18 : 0x80a , # CONTENT_COBBLE
19 : 0x80b , # CONTENT_STEEL
20 : 0x80c , # CONTENT_GLASS
22 : 0x80d , # CONTENT_MOSSYCOBBLE
23 : 0x80e , # CONTENT_GRAVEL
24 : 0x80f , # CONTENT_SANDSTONE
25 : 0x810 , # CONTENT_CACTUS
26 : 0x811 , # CONTENT_BRICK
27 : 0x812 , # CONTENT_CLAY
28 : 0x813 , # CONTENT_PAPYRUS
29 : 0x814 } # CONTENT_BOOKSHELF
def hex_to_int ( h ) :
i = int ( h , 16 )
if ( i > 2047 ) :
i - = 4096
return i
def hex4_to_int ( h ) :
i = int ( h , 16 )
if ( i > 32767 ) :
i - = 65536
return i
def int_to_hex3 ( i ) :
if ( i < 0 ) :
return " %03X " % ( i + 4096 )
else :
return " %03X " % i
def int_to_hex4 ( i ) :
if ( i < 0 ) :
return " %04X " % ( i + 65536 )
else :
return " %04X " % i
#def signedToUnsigned(i, max_positive):
# if i >= 0:
# return i
# else:
# return i + 2*max_positive
#def getBlockAsInteger(p):
# return signedToUnsigned(p[2],2048)*16777216 + signedToUnsigned(p[1],2048)*4096 + signedToUnsigned(p[0],2048)
def getBlockAsInteger ( p ) :
return p [ 2 ] * 16777216 + p [ 1 ] * 4096 + p [ 0 ]
def unsignedToSigned ( i , max_positive ) :
if i < max_positive :
return i
else :
return i - 2 * max_positive
def getIntegerAsBlock ( i ) :
x = unsignedToSigned ( i % 4096 , 2048 )
i = int ( ( i - x ) / 4096 )
y = unsignedToSigned ( i % 4096 , 2048 )
i = int ( ( i - y ) / 4096 )
z = unsignedToSigned ( i % 4096 , 2048 )
return x , y , z
def readU8 ( f ) :
return ord ( f . read ( 1 ) )
def readU16 ( f ) :
return ord ( f . read ( 1 ) ) * 256 + ord ( f . read ( 1 ) )
def readU32 ( f ) :
return ord ( f . read ( 1 ) ) * 256 * 256 * 256 + ord ( f . read ( 1 ) ) * 256 * 256 + ord ( f . read ( 1 ) ) * 256 + ord ( f . read ( 1 ) )
def readS32 ( f ) :
return unsignedToSigned ( ord ( f . read ( 1 ) ) * 256 * 256 * 256 + ord ( f . read ( 1 ) ) * 256 * 256 + ord ( f . read ( 1 ) ) * 256 + ord ( f . read ( 1 ) ) , 2 * * 31 )
CONTENT_WATER = 2
def content_is_ignore ( d ) :
return d == 0
#return d in [0, "ignore"]
def content_is_water ( d ) :
return ( d == 2 ) | ( d == 9 )
#return d in [2, 9]
def content_is_air ( d ) :
return ( d == 126 ) | ( d == 127 ) | ( d == 254 )
# return d in [126, 127, 254, "air"]
#NOT USED
def read_content ( mapdata , version , datapos = None ) :
if datapos == None :
if version > = 24 :
mapdata = numpy . array ( mapdata )
x = numpy . arange ( 4096 )
return ( mapdata [ x * 2 ] << 8 ) | ( mapdata [ x * 2 + 1 ] )
if version > = 24 :
return ( mapdata [ datapos * 2 ] << 8 ) | ( mapdata [ datapos * 2 + 1 ] )
elif version > = 20 :
if mapdata [ datapos ] < 0x80 :
return mapdata [ datapos ]
else :
return ( mapdata [ datapos ] << 4 ) | ( mapdata [ datapos + 0x2000 ] >> 4 )
elif 16 < = version < 20 :
return TRANSLATION_TABLE . get ( mapdata [ datapos ] , mapdata [ datapos ] )
else :
raise Exception ( " Unsupported map format: " + str ( version ) )
def parse_args ( ) :
parser = argparse . ArgumentParser ( description = ' A mapper for minetest ' )
parser . add_argument ( ' --bgcolor ' , default = ' black ' , metavar = ' COLOR ' , type = ImageColor . getrgb , help = ' set the background color (e.g. white or " #FFFFFF " ) ' )
parser . add_argument ( ' --scalecolor ' , default = ' white ' , metavar = ' COLOR ' , type = ImageColor . getrgb , help = ' set the ruler and text color for the scale ' )
parser . add_argument ( ' --origincolor ' , default = ' red ' , metavar = ' COLOR ' , type = ImageColor . getrgb , help = ' set the color for the map origin ' )
parser . add_argument ( ' --playercolor ' , default = ' red ' , metavar = ' COLOR ' , type = ImageColor . getrgb , help = ' set the color for player markers ' )
parser . add_argument ( ' --fogcolor ' , default = ' grey ' , metavar = ' COLOR ' , type = ImageColor . getrgb , help = ' set the color for fog (default grey) ' )
parser . add_argument ( ' --ugcolor ' , default = ' purple ' , metavar = ' COLOR ' , type = ImageColor . getrgb , help = ' set the color for underground areas (default purple) ' )
parser . add_argument ( ' --makethumb ' , action = ' store_const ' , const = True , default = False , help = ' create a thumbnail image in addition to the full size image with the file name <outputname>_thumb.png ' )
parser . add_argument ( ' --drawscale ' , action = ' store_const ' , const = True , default = False , help = ' draw a scale on the border of the map ' )
parser . add_argument ( ' --drawplayers ' , action = ' store_const ' , const = True , default = False , help = ' draw markers for players ' )
parser . add_argument ( ' --draworigin ' , action = ' store_const ' , const = True , default = False , help = ' draw the position of the origin (0,0) ' )
parser . add_argument ( ' --drawunderground ' , dest = ' drawunderground ' , action = ' store_const ' , const = 1 , default = 0 , help = ' draw underground areas overlaid on the map ' )
parser . add_argument ( ' --drawunderground-standalone ' , dest = ' drawunderground ' , action = ' store_const ' , const = 2 , help = ' draw underground areas as a standalone map ' )
parser . add_argument ( ' --region ' , nargs = 4 , type = int , metavar = ( ' XMIN ' , ' XMAX ' , ' ZMIN ' , ' ZMAX ' ) , default = ( - 2000 , 2000 , - 2000 , 2000 ) , help = ' set the bounding x,z coordinates for the map (units are nodes, default = -2000 2000 -2000 2000) ' )
parser . add_argument ( ' --maxheight ' , type = int , metavar = ( ' YMAX ' ) , default = 500 , help = ' don \' t draw above height YMAX (default = 500) ' )
parser . add_argument ( ' --minheight ' , type = int , metavar = ( ' YMIN ' ) , default = - 500 , help = ' don \' t draw below height YMIN (defualt = -500) ' )
parser . add_argument ( ' --pixelspernode ' , type = int , metavar = ( ' PPN ' ) , default = 1 , help = ' number of pixels per node (default = 1) ' )
parser . add_argument ( ' --facing ' , type = str , choices = ( ' up ' , ' down ' , ' north ' , ' south ' , ' east ' , ' west ' ) , default = ' down ' , help = ' direction to face when drawing (north, south, east or west will draw a cross-section) ' )
parser . add_argument ( ' --fog ' , type = float , metavar = ( ' FOGSTRENGTH ' ) , default = 0.0 , help = ' use fog strength of FOGSTRENGTH (0.0 by default, max of 1.0) ' )
parser . add_argument ( ' world_dir ' , help = ' the path to the world you want to map ' )
parser . add_argument ( ' output ' , nargs = ' ? ' , default = ' map.png ' , help = ' the output filename ' )
args = parser . parse_args ( )
if args . world_dir is None :
print ( " Please select world path (eg. -i ../worlds/yourworld) (or use --help) " )
sys . exit ( 1 )
if not os . path . isdir ( args . world_dir ) :
print ( " World does not exist " )
sys . exit ( 1 )
args . world_dir = os . path . abspath ( args . world_dir ) + os . path . sep
return args
# Load color information for the blocks.
def load_colors ( fname = " colors.txt " ) :
uid_to_color = { }
str_to_uid = { }
uid = 2 #unique id, we always use ignore == 0, air == 1 because these are never drawn
try :
f = open ( " colors.txt " )
except IOError :
f = open ( os . path . join ( os . path . dirname ( __file__ ) , " colors.txt " ) )
for line in f :
values = line . split ( )
if len ( values ) < 4 :
continue
identifier = values [ 0 ]
is_hex = True
for c in identifier :
if c not in " 0123456789abcdefABCDEF " :
is_hex = False
break
if is_hex :
str_to_uid [ int ( values [ 0 ] , 16 ) ] = uid
uid_to_color [ uid ] = (
int ( values [ 1 ] ) ,
int ( values [ 2 ] ) ,
int ( values [ 3 ] ) )
else :
str_to_uid [ values [ 0 ] ] = uid
uid_to_color [ uid ] = (
int ( values [ 1 ] ) ,
int ( values [ 2 ] ) ,
int ( values [ 3 ] ) )
uid + = 1
f . close ( )
return uid_to_color , str_to_uid
#print("colors: "+repr(colors))
#sys.exit(1)
def legacy_fetch_sector_data ( args , sectortype , sector_data , ypos ) :
yhex = int_to_hex4 ( ypos )
if sectortype == " old " :
filename = args . world_dir + " sectors/ " + sector_data [ 0 ] + " / " + yhex . lower ( )
else :
filename = args . world_dir + " sectors2/ " + sector_data [ 1 ] + " / " + yhex . lower ( )
return open ( filename , " rb " )
def legacy_sector_scan ( args , sectors_xmin , sector_xmax , sector_zmin , sector_zmax ) :
if os . path . exists ( args . world_dir + " sectors2 " ) :
for filename in os . listdir ( args . world_dir + " sectors2 " ) :
for filename2 in os . listdir ( args . world_dir + " sectors2/ " + filename ) :
x = hex_to_int ( filename )
z = hex_to_int ( filename2 )
if x < sector_xmin or x > sector_xmax :
continue
if z < sector_zmin or z > sector_zmax :
continue
xlist . append ( x )
zlist . append ( z )
if os . path . exists ( args . world_dir + " sectors " ) :
for filename in os . listdir ( args . world_dir + " sectors " ) :
x = hex4_to_int ( filename [ : 4 ] )
z = hex4_to_int ( filename [ - 4 : ] )
if x < sector_xmin or x > sector_xmax :
continue
if z < sector_zmin or z > sector_zmax :
continue
xlist . append ( x )
zlist . append ( z )
def legacy_fetch_ylist ( args , xpos , zpos , ylist ) :
sectortype = " "
xhex = int_to_hex3 ( xpos )
zhex = int_to_hex3 ( zpos )
xhex4 = int_to_hex4 ( xpos )
zhex4 = int_to_hex4 ( zpos )
sector1 = xhex4 . lower ( ) + zhex4 . lower ( )
sector2 = xhex . lower ( ) + " / " + zhex . lower ( )
try :
for filename in os . listdir ( args . world_dir + " sectors/ " + sector1 ) :
if ( filename != " meta " ) :
pos = int ( filename , 16 )
if ( pos > 32767 ) :
pos - = 65536
ylist . append ( pos )
if len ( ylist ) > 0 :
sectortype = " old "
if sectortype == " " :
try :
for filename in os . listdir ( args . world_dir + " sectors2/ " + sector2 ) :
if ( filename != " meta " ) :
pos = int ( filename , 16 )
if ( pos > 32767 ) :
pos - = 65536
ylist . append ( pos )
sectortype = " new "
except OSError :
pass
except OSError :
pass
return sectortype
#Alternative map_block
def find ( arr , value , axis = - 1 ) :
return ( ( arr == value ) . cumsum ( axis = axis ) == 0 ) . sum ( axis = axis )
#
# if False:
# mapdata = numpy.swapaxes(mapdata.reshape(16,16,16),0,2)
# mapdata = numpy.swapaxes(mapdata,1,2).reshape(256,16)
# content = mapdata[plist]
# opaques = ~( (content == ignore) | (content == air) )
# h = find(opaques,True,1)
# po = (h<16)
# hpo = h[po]
# hdata[po] = chunkypos + 16 - hpo
# cdata[po] = content[po][:,hpo]
# dnddata[po] = day_night_differs
# plist = plist[~po]
def map_block ( mapdata , version , ypos , maxy , plist , cdata , hdata , dnddata , day_night_differs , id_map , ignore , air , face_swap_order ) :
chunkypos = ypos * 16
mapdata = mapdata [ : 4096 ]
mapdata = id_map [ mapdata ]
if ( mapdata == ignore ) . all ( ) :
## return (~( (cdata == ignore) | (cdata == air) )).all()
return plist
( swap1a , swap1b ) , ( swap2a , swap2b ) = face_swap_order [ 1 : ]
mapdata = numpy . swapaxes ( mapdata . reshape ( 16 , 16 , 16 ) , swap1a , swap1b )
mapdata = numpy . swapaxes ( mapdata , swap2a , swap2b ) . reshape ( 16 , 256 )
if face_swap_order [ 0 ] > 0 :
r = range ( maxy , - 1 , - 1 )
else :
r = range ( maxy , 16 , 1 )
# mapdata=mapdata[::-1]
y = maxy
# if True:
# mapdata = mapdata[y:]
# opaques = ~( (mapdata == ignore) | (mapdata == air) )
# copaques = ~( (cdata == ignore) | (cdata == air) )
# h = find(opaques,True,0)
# po = (h<16-y)
# hpo = h*po
# hdata[~copaques] = chunkypos + 16 - hpo[~copaques]
# cdata[~copaques] = mapdata[hpo][~copaques]
# dnddata[~copaques] = day_night_differs
# if (~( (cdata == ignore) | (cdata == air) )).all():
# return []
# else:
# return plist
for y in r :
if len ( plist ) == 0 :
break
content = mapdata [ y ] [ plist ]
# watercontent = content_is_water(content)
# wdata[plist] += watercontent
# opaques = ~( (content_is_air(content) | content_is_ignore(content) | watercontent))
opaques = ~ ( ( content == ignore ) | ( content == air ) )
po = plist [ opaques ]
pno = plist [ ~ opaques ]
cdata [ po ] = content [ opaques ]
hdata [ po ] = chunkypos + y
dnddata [ po ] = day_night_differs
plist = plist [ ~ opaques ]
y - = 1
return plist
def map_block_ug ( mapdata , version , ypos , maxy , cdata , hdata , udata , uhdata , dnddata , day_night_differs , id_map , ignore , air , underground , face_swap_order ) :
chunkypos = ypos * 16
mapdata = mapdata [ : 4096 ]
mapdata = id_map [ mapdata ]
if ( mapdata == ignore ) . all ( ) :
return ( ~ ( ( cdata == ignore ) | ( cdata == air ) ) ) . all ( )
( swap1a , swap1b ) , ( swap2a , swap2b ) = face_swap_order [ 1 : ]
mapdata = numpy . swapaxes ( mapdata . reshape ( 16 , 16 , 16 ) , swap1a , swap1b )
mapdata = numpy . swapaxes ( mapdata , swap2a , swap2b ) . reshape ( 16 , 256 )
if face_swap_order [ 0 ] > 0 :
r = range ( maxy , - 1 , - 1 )
else :
r = range ( maxy , 16 , 1 )
y = maxy
for y in r :
content = mapdata [ y ]
opaques = ~ ( ( content == ignore ) | ( content == air ) )
copaques = ~ ( ( cdata == ignore ) | ( cdata == air ) )
air = ( content == air )
cdata [ ~ copaques ] = content [ ~ copaques ]
hdata [ ~ copaques ] = chunkypos + y
dnddata [ ~ copaques ] = day_night_differs
uhdata + = ( udata == 0 ) * ( chunkypos + y ) * ( air * copaques ) * ( ~ opaques ) * underground
udata + = ( air * copaques ) * ( ~ opaques ) * underground
return ( ~ ( ( cdata == ignore ) | ( cdata == air ) ) ) . all ( )
# y-=1
def get_db ( args ) :
if not os . path . exists ( args . world_dir + " world.mt " ) :
return None
with open ( args . world_dir + " world.mt " ) as f :
keyvals = f . read ( ) . splitlines ( )
keyvals = [ kv . split ( " = " ) for kv in keyvals ]
backend = None
for k , v in keyvals :
if k . strip ( ) == " backend " :
backend = v . strip ( )
break
if backend == " sqlite3 " :
return SQLDB ( args . world_dir + " map.sqlite " )
if backend == " leveldb " :
return LVLDB ( args . world_dir + " map.db " )
class SQLDB :
def __init__ ( self , path ) :
import sqlite3
conn = sqlite3 . connect ( path )
self . cur = conn . cursor ( )
def __iter__ ( self ) :
self . cur . execute ( " SELECT `pos` FROM `blocks` " )
while True :
r = self . cur . fetchone ( )
if not r :
break
x , y , z = getIntegerAsBlock ( r [ 0 ] )
yield x , y , z , r [ 0 ]
def get ( self , pos ) :
self . cur . execute ( " SELECT `data` FROM `blocks` WHERE `pos`==? LIMIT 1 " , ( pos , ) )
r = self . cur . fetchone ( )
if not r :
return
return BytesIO ( r [ 0 ] )
class LVLDB :
def __init__ ( self , path ) :
import leveldb
self . conn = leveldb . LevelDB ( path )
def __iter__ ( self ) :
for k in self . conn . RangeIter ( ) :
x , y , z = getIntegerAsBlock ( int ( k [ 0 ] ) )
yield x , y , z , k [ 0 ]
def get ( self , pos ) :
return BytesIO ( self . conn . Get ( pos ) )
class World :
def __init__ ( self , args ) :
self . xlist = [ ]
self . zlist = [ ]
self . args = args
self . db = None
self . minx = None
self . minz = None
self . maxx = None
self . maxz = None
self . mapinfo = None
def facing ( self , x , y , z ) :
if self . args . facing in [ ' up ' , ' down ' ] :
return x , y , z
if self . args . facing in [ ' east ' , ' west ' ] :
return z , x , y
if self . args . facing in [ ' north ' , ' south ' ] :
return x , z , y
def generate_sector_list ( self ) :
'''
List all sectors to memory and calculate the width and heigth of the
resulting picture .
'''
args = self . args
sector_xmin , sector_xmax , sector_zmin , sector_zmax = numpy . array ( args . region ) / 16
sector_ymin = args . minheight / 16
sector_ymax = args . maxheight / 16
xlist = [ ]
zlist = [ ]
self . lookup = { }
self . db = get_db ( args )
if self . db is not None :
for x , y , z , pos in self . db :
if x < sector_xmin or x > sector_xmax :
continue
if z < sector_zmin or z > sector_zmax :
continue
if y < sector_ymin or y > sector_ymax :
continue
x , y , z = self . facing ( x , y , z )
try :
self . lookup [ ( x , z ) ] . append ( ( y , pos ) )
except KeyError :
self . lookup [ ( x , z ) ] = [ ( y , pos ) ]
xlist . append ( x )
zlist . append ( z )
else :
legacy_sector_scan ( args , sectors_xmin , sector_xmax , sector_zmin , sector_zmax )
if len ( xlist ) > 0 :
# Get rid of duplicates
self . xlist , self . zlist = zip ( * sorted ( set ( zip ( xlist , zlist ) ) ) )
self . minx = min ( xlist )
self . minz = min ( zlist )
self . maxx = max ( xlist )
self . maxz = max ( zlist )
x0 , x1 , z0 , z1 = numpy . array ( args . region )
y0 = args . minheight
y1 = args . maxheight
self . minypos = self . facing ( int ( x0 ) , int ( y0 ) , int ( z0 ) ) [ 1 ]
self . maxypos = self . facing ( int ( x1 ) , int ( y1 ) , int ( z1 ) ) [ 1 ]
self . w = ( self . maxx - self . minx ) * 16 + 16
self . h = ( self . maxz - self . minz ) * 16 + 16
def generate_map_info ( self , str_to_uid ) :
read_map_time = 0
db = self . db
xlist = self . xlist
zlist = self . zlist
args = self . args
minx = self . minx
minz = self . minz
maxx = self . maxx
maxz = self . maxz
w = self . w
h = self . h
#x,y,z becomes y,x,z for up/down
# becomes x,z,y for east/west
# becomes z,x,y for north/south
if args . facing in [ ' up ' , ' down ' ] :
face_swap_order = [ 1 , ( 1 , 0 ) , ( 1 , 2 ) ]
elif args . facing in [ ' east ' , ' west ' ] :
face_swap_order = [ 1 , ( 2 , 0 ) , ( 2 , 1 ) ]
elif args . facing in [ ' north ' , ' south ' ] :
face_swap_order = [ 1 , ( 0 , 0 ) , ( 1 , 2 ) ]
if args . facing in [ ' up ' , ' east ' , ' north ' ] :
face_swap_order [ 0 ] = - 1
mapinfo = {
' height ' : numpy . zeros ( [ w , h ] , dtype = ' i2 ' ) ,
' content ' : numpy . zeros ( [ w , h ] , dtype = ' u2 ' ) ,
' water ' : numpy . zeros ( [ w , h ] , dtype = ' u2 ' ) ,
' dnd ' : numpy . zeros ( [ w , h ] , dtype = bool ) }
if args . drawunderground :
mapinfo [ ' underground ' ] = numpy . zeros ( [ w , h ] , dtype = ' u2 ' )
mapinfo [ ' undergroundh ' ] = numpy . zeros ( [ w , h ] , dtype = ' i2 ' )
unknown_node_names = set ( )
unknown_node_ids = set ( )
starttime = time . time ( )
# Go through all sectors.
for n in range ( len ( xlist ) ) :
#if n > 500:
# break
if n % 200 == 0 :
nowtime = time . time ( )
dtime = nowtime - starttime
try :
n_per_second = 1.0 * n / dtime
except ZeroDivisionError :
n_per_second = 0
if n_per_second != 0 :
seconds_per_n = 1.0 / n_per_second
time_guess = seconds_per_n * len ( xlist )
remaining_s = time_guess - dtime
remaining_minutes = int ( remaining_s / 60 )
remaining_s - = remaining_minutes * 60
print ( " Processing sector " + str ( n ) + " of " + str ( len ( xlist ) )
+ " ( " + str ( round ( 100.0 * n / len ( xlist ) , 1 ) ) + " % ) "
+ " (ETA: " + str ( remaining_minutes ) + " m "
+ str ( int ( remaining_s ) ) + " s) " )
xpos = xlist [ n ]
zpos = zlist [ n ]
ylist = [ ]
sectortype = " "
if db is not None :
ymin = self . minypos / 16 #-2048 if args.minheight is None else args.minheight/16+1
ymax = self . maxypos / 16 + 1 #2047 if args.maxheight is None else args.maxheight/16+1
for k in self . lookup [ ( xpos , zpos ) ] :
ylist . append ( k )
sectortype = " sqlite "
else :
sectortype , sector_data = legacy_fetch_ylist ( args , xpos , zpos , ylist )
if sectortype == " " :
continue
ylist . sort ( )
if face_swap_order [ 0 ] > 0 :
ylist . reverse ( )
if args . facing in [ ' south ' , ' west ' , ' down ' ] :
miny = self . minypos - 1
else :
miny = self . maxypos + 1
# Create map related info for the sector that will be filled as we seek down the y axis
cdata = numpy . zeros ( 256 , dtype = ' i4 ' )
hdata = numpy . ones ( 256 , dtype = ' i4 ' ) * miny
wdata = numpy . zeros ( 256 , dtype = ' i4 ' )
dnddata = numpy . zeros ( 256 , dtype = bool )
if args . drawunderground :
udata = numpy . zeros ( 256 , dtype = ' i4 ' )
uhdata = numpy . zeros ( 256 , dtype = ' i4 ' )
plist = numpy . arange ( 256 )
# Go through the Y axis from top to bottom.
for ypos , ps in ylist :
try :
if db is not None :
f = db . get ( ps )
else :
f = legacy_fetch_sector_data ( args , sectortype , sector_data , ypos )
# Let's just memorize these even though it's not really necessary.
version = readU8 ( f )
flags = f . read ( 1 )
#print("version="+str(version))
#print("flags="+str(version))
# Check flags
is_underground = ( ( ord ( flags ) & 1 ) != 0 )
day_night_differs = ( ( ord ( flags ) & 2 ) != 0 )
lighting_expired = ( ( ord ( flags ) & 4 ) != 0 )
generated = ( ( ord ( flags ) & 8 ) != 0 )
#print("is_underground="+str(is_underground))
#print("day_night_differs="+str(day_night_differs))
#print("lighting_expired="+str(lighting_expired))
#print("generated="+str(generated))
if version > = 22 :
content_width = readU8 ( f )
params_width = readU8 ( f )
# Node data
dec_o = zlib . decompressobj ( )
try :
s = dec_o . decompress ( f . read ( ) )
mapdata = numpy . fromstring ( s , " >u2 " )
except :
mapdata = [ ]
# Reuse the unused tail of the file
f . close ( ) ;
f = BytesIO ( dec_o . unused_data )
#print("unused data: "+repr(dec_o.unused_data))
# zlib-compressed node metadata list
dec_o = zlib . decompressobj ( )
try :
s = dec_o . decompress ( f . read ( ) )
metaliststr = numpy . fromstring ( s , " u1 " )
# And do nothing with it
except :
metaliststr = [ ]
# Reuse the unused tail of the file
f . close ( ) ;
f = BytesIO ( dec_o . unused_data )
#print("* dec_o.unused_data: "+repr(dec_o.unused_data))
data_after_node_metadata = dec_o . unused_data
if version < = 21 :
# mapblockobject_count
readU16 ( f )
if version == 23 :
readU8 ( f ) # Unused node timer version (always 0)
if version == 24 :
ver = readU8 ( f )
if ver == 1 :
num = readU16 ( f )
for i in range ( 0 , num ) :
readU16 ( f )
readS32 ( f )
readS32 ( f )
static_object_version = readU8 ( f )
static_object_count = readU16 ( f )
for i in range ( 0 , static_object_count ) :
# u8 type (object type-id)
object_type = readU8 ( f )
# s32 pos_x_nodes * 10000
pos_x_nodes = readS32 ( f ) / 10000
# s32 pos_y_nodes * 10000
pos_y_nodes = readS32 ( f ) / 10000
# s32 pos_z_nodes * 10000
pos_z_nodes = readS32 ( f ) / 10000
# u16 data_size
data_size = readU16 ( f )
# u8[data_size] data
data = f . read ( data_size )
timestamp = readU32 ( f )
#print("* timestamp="+str(timestamp))
id_to_name = { }
name_to_id = { }
air = 1
ignore = 0
if version > = 22 :
name_id_mapping_version = readU8 ( f )
num_name_id_mappings = readU16 ( f )
#print("* num_name_id_mappings: "+str(num_name_id_mappings))
for i in range ( 0 , num_name_id_mappings ) :
node_id = readU16 ( f )
name_len = readU16 ( f )
name = f . read ( name_len ) . decode ( ' utf8 ' )
try :
id_to_name [ node_id ] = str_to_uid [ name ]
except :
##TODO: Add to list of unknown colors
unknown_node_names . add ( name )
unknown_node_ids . add ( node_id )
id_to_name [ node_id ] = 0
if name == ' air ' :
air = id_to_name [ node_id ]
if name == ' ignore ' :
ignore = id_to_name [ node_id ]
if len ( id_to_name ) == 0 :
id_map = numpy . array ( [ 0 , 1 ] , dtype = ' i4 ' )
else :
id_map = numpy . array ( [ id_to_name [ i ] for i in sorted ( id_to_name ) ] , dtype = ' i4 ' )
# Node timers
if version > = 25 :
timer_size = readU8 ( f )
num = readU16 ( f )
for i in range ( 0 , num ) :
readU16 ( f )
readS32 ( f )
readS32 ( f )
##facing in down,south,west use maxheight, otherwise use minheight
if face_swap_order [ 0 ] > 0 :
maxy = 15
if ypos * 16 + 15 > self . maxypos :
maxy = self . maxypos - ypos * 16
else :
maxy = 0
if ypos * 16 + 15 < self . minypos :
maxy = ypos * 16 - self . minypos
if maxy > = 0 :
if args . drawunderground :
plist = map_block_ug ( mapdata , version , ypos , maxy , cdata , hdata , udata , uhdata , dnddata , day_night_differs , id_map , ignore , air , is_underground , face_swap_order )
else :
plist = map_block ( mapdata , version , ypos , maxy , plist , cdata , hdata , dnddata , day_night_differs , id_map , ignore , air , face_swap_order )
##plist = map_block(mapdata, version, ypos, maxy, cdata, hdata, dnddata, day_night_differs, id_map, ignore, air, face_swap_order)
# After finding all the pixels in the sector, we can move on to
# the next sector without having to continue the Y axis.
if ( not args . drawunderground and len ( plist ) == 0 ) or ypos == ylist [ - 1 ] [ 0 ] :
##if plist == True or ypos==ylist[-1][0]:
chunkxpos = ( xpos - minx ) * 16
chunkzpos = ( zpos - minz ) * 16
if True : #face_swap_order[0]<0:
pass
#chunkxpos = (maxx-minx)*16 - chunkxpos #-16?
#chunkzpos = (maxz-minz)*16 - chunkzpos #-16?
pos = ( slice ( chunkxpos , chunkxpos + 16 ) , slice ( chunkzpos , chunkzpos + 16 ) )
mapinfo [ ' height ' ] [ pos ] = hdata . reshape ( 16 , 16 )
mapinfo [ ' content ' ] [ pos ] = cdata . reshape ( 16 , 16 )
mapinfo [ ' water ' ] [ pos ] = wdata . reshape ( 16 , 16 )
mapinfo [ ' dnd ' ] [ pos ] = dnddata . reshape ( 16 , 16 )
if args . drawunderground :
mapinfo [ ' underground ' ] [ pos ] = udata . reshape ( 16 , 16 )
mapinfo [ ' undergroundh ' ] [ pos ] = uhdata . reshape ( 16 , 16 )
break
except Exception as e :
print ( " Error at ( " + str ( xpos ) + " , " + str ( ypos ) + " , " + str ( zpos ) + " ): " + str ( e ) )
traceback . print_exc ( )
sys . stdout . write ( " Block data: " )
for c in r [ 0 ] :
sys . stdout . write ( " %2.2x " % ord ( c ) )
sys . stdout . write ( os . linesep )
sys . stdout . write ( " Data after node metadata: " )
for c in data_after_node_metadata :
sys . stdout . write ( " %2.2x " % ord ( c ) )
sys . stdout . write ( os . linesep )
traceback . print_exc ( )
self . mapinfo = mapinfo
if unknown_node_names :
sys . stdout . write ( " Unknown node names: " )
for name in unknown_node_names :
sys . stdout . write ( " " + name )
sys . stdout . write ( os . linesep )
if unknown_node_ids :
sys . stdout . write ( " Unknown node ids: " )
for node_id in unknown_node_ids :
sys . stdout . write ( " " + str ( hex ( node_id ) ) )
sys . stdout . write ( os . linesep )
# print str_to_uid
def draw_image ( world , uid_to_color ) :
# Drawing the picture
args = world . args
stuff = world . mapinfo
minx = world . minx
minz = world . minz
maxx = world . maxx
maxz = world . maxz
w = world . w
h = world . h
reverse_dirs = [ ' east ' , ' south ' , ' up ' ]
print ( " Drawing image " )
starttime = time . time ( )
border = 40 if args . drawscale else 0
im = Image . new ( " RGB " , ( w * args . pixelspernode + border , h * args . pixelspernode + border ) , args . bgcolor )
draw = ImageDraw . Draw ( im )
if args . pixelspernode > 1 :
stuff [ ' content ' ] = stuff [ ' content ' ] . repeat ( args . pixelspernode , axis = 0 ) . repeat ( args . pixelspernode , axis = 1 )
stuff [ ' dnd ' ] = stuff [ ' dnd ' ] . repeat ( args . pixelspernode , axis = 0 ) . repeat ( args . pixelspernode , axis = 1 )
stuff [ ' height ' ] = stuff [ ' height ' ] . repeat ( args . pixelspernode , axis = 0 ) . repeat ( args . pixelspernode , axis = 1 )
stuff [ ' water ' ] = stuff [ ' water ' ] . repeat ( args . pixelspernode , axis = 0 ) . repeat ( args . pixelspernode , axis = 1 )
if args . facing in reverse_dirs :
stuff [ ' content ' ] = stuff [ ' content ' ] [ : : - 1 , : ]
stuff [ ' dnd ' ] = stuff [ ' dnd ' ] [ : : - 1 , : ]
stuff [ ' height ' ] = stuff [ ' height ' ] [ : : - 1 , : ]
stuff [ ' water ' ] = stuff [ ' water ' ] [ : : - 1 , : ]
count_dnd = 0
count_height = 0
count_zero = 0
c = stuff [ ' content ' ]
dnd = stuff [ ' dnd ' ]
hgh = stuff [ ' height ' ]
c0 = c [ 1 : , : - 1 ]
c1 = c [ : - 1 , 1 : ]
c2 = c [ 1 : , 1 : ]
dnd0 = dnd [ 1 : , : - 1 ]
dnd1 = dnd [ : - 1 , 1 : ]
dnd2 = dnd [ 1 : , 1 : ]
h0 = hgh [ 1 : , : - 1 ]
h1 = hgh [ : - 1 , 1 : ]
h2 = hgh [ 1 : , 1 : ]
drop = ( 2 * h0 - h1 - h2 ) * 12
if args . facing in [ ' east ' , ' north ' , ' up ' ] :
drop = - drop
drop = numpy . clip ( drop , - 32 , 32 )
if args . fog > 0 :
fogstrength = 1.0 * ( stuff [ ' height ' ] - stuff [ ' height ' ] . min ( ) ) / ( stuff [ ' height ' ] . max ( ) - stuff [ ' height ' ] . min ( ) )
if args . facing in reverse_dirs :
fogstrength = 1 - fogstrength
fogstrength = args . fog * fogstrength
fogstrength = fogstrength [ : , : , numpy . newaxis ]
if args . drawunderground :
ugcoeff = 0.9 if args . drawunderground == 2 else 0.4
ugstrength = 1.0 * ( stuff [ ' underground ' ] ) / 6 #normalize so that 6 blocks of air underground is considered "big"
ugstrength = ( ugstrength > 0 ) * 0.1 + ugcoeff * ugstrength
ugstrength = ugstrength - ( ugstrength - 0.75 ) * ( ugstrength > 0.75 )
ugstrength = ugstrength [ : , : , numpy . newaxis ]
print ( ' ugmin ' , stuff [ ' undergroundh ' ] . min ( ) )
print ( ' ugmax ' , stuff [ ' undergroundh ' ] . max ( ) )
ugdepth = 1.0 * ( stuff [ ' undergroundh ' ] - stuff [ ' undergroundh ' ] . min ( ) ) / ( stuff [ ' undergroundh ' ] . max ( ) - stuff [ ' undergroundh ' ] . min ( ) )
ugdepth = ugdepth [ : , : , numpy . newaxis ]
u = stuff [ ' underground ' ]
u0 = u [ 1 : , : - 1 ] > 0
u1 = u [ : - 1 , 1 : ] > 0
u2 = u [ 1 : , 1 : ] > 0
hgh = stuff [ ' undergroundh ' ]
h0 = hgh [ 1 : , : - 1 ]
h1 = hgh [ : - 1 , 1 : ]
h2 = hgh [ 1 : , 1 : ]
dropg = ( 2 * h0 - h1 - h2 ) * 12 * u0 * u1 * u2
if args . facing in reverse_dirs :
dropg = - dropg
dropg = numpy . clip ( dropg , - 32 , 32 )
if args . drawunderground < 2 : #normal map or cave with map overlay
colors = numpy . array ( [ args . bgcolor , args . bgcolor ] + [ uid_to_color [ c ] for c in sorted ( uid_to_color ) ] , dtype = ' i2 ' )
else :
colors = numpy . array ( [ args . bgcolor , args . bgcolor ] + [ args . bgcolor for c in sorted ( uid_to_color ) ] , dtype = ' i2 ' )
pix = colors [ stuff [ ' content ' ] ]
if args . drawunderground < 2 :
pix [ 1 : , : - 1 ] + = drop [ : , : , numpy . newaxis ]
pix = numpy . clip ( pix , 0 , 255 )
if args . fog > 0 :
pix = args . fogcolor * fogstrength + pix * ( 1 - fogstrength )
pix = numpy . clip ( pix , 0 , 255 )
if args . drawunderground :
ugpd = args . ugcolor * ugdepth + args . bgcolor * ( 1 - ugdepth ) ##average with background color based on depth (deeper caves will be more bg color)
pix = ugpd * ugstrength + pix * ( 1 - ugstrength )
pix [ 1 : , : - 1 ] + = dropg [ : , : , numpy . newaxis ]
pix = numpy . clip ( pix , 0 , 255 )
pix = numpy . array ( pix , dtype = ' u1 ' )
impix = Image . fromarray ( pix , ' RGB ' )
impix = impix . transpose ( Image . ROTATE_90 )
im . paste ( impix , ( border , border ) )
if args . draworigin :
if args . facing in [ ' east ' , ' north ' , ' up ' ] :
draw . ellipse ( ( ( w - ( minx * - 16 - 5 ) ) * args . pixelspernode + border , ( h - minz * - 16 - 6 ) * args . pixelspernode + border ,
( w - ( minx * - 16 + 5 ) ) * args . pixelspernode + border , ( h - minz * - 16 + 4 ) ) * args . pixelspernode + border ,
outline = args . origincolor )
else :
draw . ellipse ( ( ( minx * - 16 - 5 ) * args . pixelspernode + border , ( h - minz * - 16 - 6 ) * args . pixelspernode + border ,
( minx * - 16 + 5 ) * args . pixelspernode + border , ( h - minz * - 16 + 4 ) * args . pixelspernode + border ) ,
outline = args . origincolor )
font = ImageFont . load_default ( )
if args . drawscale :
if args . facing in [ ' up ' , ' down ' ] :
draw . text ( ( 24 , 0 ) , " X " , font = font , fill = args . scalecolor )
draw . text ( ( 2 , 24 ) , " Z " , font = font , fill = args . scalecolor )
elif args . facing in [ ' east ' , ' west ' ] :
draw . text ( ( 24 , 0 ) , " Z " , font = font , fill = args . scalecolor )
draw . text ( ( 2 , 24 ) , " Y " , font = font , fill = args . scalecolor )
elif args . facing in [ ' north ' , ' south ' ] :
draw . text ( ( 24 , 0 ) , " X " , font = font , fill = args . scalecolor )
draw . text ( ( 2 , 24 ) , " Y " , font = font , fill = args . scalecolor )
if args . facing in reverse_dirs :
for n in range ( int ( minx / - 4 ) * - 4 , maxx + 1 , 4 ) :
draw . text ( ( ( w - ( minx * - 16 + n * 16 ) ) * args . pixelspernode + border + 2 , 0 ) , str ( n * 16 ) ,
font = font , fill = args . scalecolor )
draw . line ( ( ( w - ( minx * - 16 + n * 16 ) ) * args . pixelspernode + border , 0 ,
( w - ( minx * - 16 + n * 16 ) ) * args . pixelspernode + border , border - 1 ) , fill = args . scalecolor )
else :
for n in range ( int ( minx / - 4 ) * - 4 , maxx , 4 ) :
draw . text ( ( ( minx * - 16 + n * 16 ) * args . pixelspernode + border + 2 , 0 ) , str ( n * 16 ) ,
font = font , fill = args . scalecolor )
draw . line ( ( ( minx * - 16 + n * 16 ) * args . pixelspernode + border , 0 ,
( minx * - 16 + n * 16 ) * args . pixelspernode + border , border - 1 ) , fill = args . scalecolor )
for n in range ( int ( maxz / 4 ) * 4 , minz , - 4 ) :
draw . text ( ( 2 , ( h - 1 - ( n * 16 - minz * 16 ) ) * args . pixelspernode + border ) , str ( n * 16 ) ,
font = font , fill = args . scalecolor )
draw . line ( ( 0 , ( h - 1 - ( n * 16 - minz * 16 ) ) * args . pixelspernode + border , border - 1 ,
( h - 1 - ( n * 16 - minz * 16 ) ) * args . pixelspernode + border ) , fill = args . scalecolor )
if args . drawplayers :
try :
for filename in os . listdir ( args . world_dir + " players " ) :
f = open ( args . world_dir + " players/ " + filename )
lines = f . readlines ( )
name = " "
position = [ ]
for line in lines :
p = line . split ( )
if p [ 0 ] == " name " :
name = p [ 2 ]
print ( filename + " : name = " + name )
if p [ 0 ] == " position " :
position = p [ 2 ] [ 1 : - 1 ] . split ( " , " )
print ( filename + " : position = " + p [ 2 ] )
if len ( name ) < 0 and len ( position ) == 3 :
x , y , z = [ int ( float ( p ) / 10 ) for p in position ]
x , y , z = world . facing ( x , y , z )
if args . facing in reverse_dirs :
x = ( w - x - minx * 16 ) * args . pixelspernode
z = ( h - z - minz * 16 ) * args . pixelspernode
else :
x = ( x - minx * 16 ) * args . pixelspernode
z = ( h - z - minz * 16 ) * args . pixelspernode
draw . ellipse ( ( ( x - 2 ) * args . pixelspernode + border , ( z - 2 ) * args . pixelspernode + border ,
( x + 2 ) * args . pixelspernode + border , ( z + 2 ) * args . pixelspernode + border ) , outline = args . playercolor )
draw . text ( ( ( x + 2 ) * args . pixelspernode + border , ( z + 2 ) * args . pixelspernode + border ) , name ,
font = font , fill = args . playercolor )
f . close ( )
except OSError :
pass
# worldlimits are measured in cubes of 16x16x16
pngminx = minx * 16
pngmaxx = maxx * 16
pngminz = minz * 16
pngmaxz = maxz * 16
pngregion = [ pngminx , pngmaxx , pngminz , pngmaxz ]
print ( " Saving to: " + args . output )
print ( " PNG Region: " , pngregion )
print ( " pngMinX: " , str ( pngminx ) )
print ( " pngMaxZ: " , str ( pngmaxz ) )
print ( " Pixels PerNode: " , args . pixelspernode )
print ( " border: " , border )
# This saves data in tEXt chunks (non-standard naming tags are allowed according to the PNG specification)
im . info [ " pngRegion " ] = str ( pngregion [ 0 ] ) + " , " + str ( pngregion [ 1 ] ) + " , " + str ( pngregion [ 2 ] ) + " , " + str ( pngregion [ 3 ] )
im . info [ " pngMinX " ] = str ( pngminx )
im . info [ " pngMaxZ " ] = str ( pngmaxz )
im . info [ " border " ] = str ( border )
im . info [ " pixPerNode " ] = str ( args . pixelspernode )
pngsave ( im , args . output )
if args . makethumb :
# Now create a square 'thumbnail' for display on square faces (which turns out to benefit from quite high resolution).
thumbSize = 512
imSize = im . size
print imSize
if imSize [ 0 ] > imSize [ 1 ] :
reSize = ( thumbSize , int ( thumbSize * ( float ( imSize [ 1 ] ) / imSize [ 0 ] ) ) )
else :
reSize = ( int ( thumbSize * ( float ( imSize [ 0 ] ) / imSize [ 1 ] ) ) , thumbSize )
print reSize
thumbBorder = ( ( thumbSize - reSize [ 0 ] ) / 2 , ( thumbSize - reSize [ 1 ] ) / 2 , thumbSize - ( thumbSize - reSize [ 0 ] ) / 2 , thumbSize - ( thumbSize - reSize [ 1 ] ) / 2 )
print thumbBorder
thumbIm = Image . new ( " RGB " , ( thumbSize , thumbSize ) , args . bgcolor )
thumbIm . paste ( im . resize ( reSize ) , thumbBorder )
thumbIm . save ( os . path . splitext ( args . output ) [ 0 ] + " _thumb.png " , " PNG " )
def main ( ) :
args = parse_args ( )
uid_to_color , str_to_uid = load_colors ( )
world = World ( args )
world . generate_sector_list ( )
if len ( world . xlist ) == 0 :
print ( " World data does not exist. " )
sys . exit ( 1 )
print ( " Result image (w= " + str ( world . w ) + " h= " + str ( world . h ) + " ) will be written to "
+ args . output )
world . generate_map_info ( str_to_uid )
draw_image ( world , uid_to_color )
if __name__ == ' __main__ ' :
main ( )