From e7cd2669805a36596c0e71d77165cd38b18b48b3 Mon Sep 17 00:00:00 2001 From: Jacob Gustafson <7557867+expertmm@users.noreply.github.com> Date: Thu, 15 Feb 2018 16:40:18 -0500 Subject: [PATCH] commit last maintained spillz version from https://github.com/spillz/minetest/blob/master/util/minetestmapper-numpy.py --- minetestmapper-numpy.py | 1070 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 1070 insertions(+) create mode 100644 minetestmapper-numpy.py diff --git a/minetestmapper-numpy.py b/minetestmapper-numpy.py new file mode 100644 index 0000000..51f4813 --- /dev/null +++ b/minetestmapper-numpy.py @@ -0,0 +1,1070 @@ +#!/usr/bin/env python2 +# -*- coding: utf-8 -*- + +# This program 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 the Do What The Fuck You Want +# To Public License, Version 2, as published by Sam Hocevar. See +# COPYING for more details. + +# 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 _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("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()