#!/usr/bin/env python3 # -*- coding: utf-8 -*- """Make an image using minetest data (numpy-ized by spillz). Authors: spillz, modified by: Poikilos; based on minetestmapper by Jogge and modified by celeron55. License: See LICENSE """ import zlib import os import string import time import argparse import sys import traceback import numpy import itertools import io try: BytesIO = io.BytesIO except AttributeError: BytesIO = io.StringIO PIL_HELP = """ You must first install Pillow (fork of PIL). - On Windows: Right-click windows menu, 'Command Prompt (Admin)' then: pip install Pillow - On *nix-like systems: sudo python2 -m pip install --upgrade pip sudo python2 -m pip install --upgrade pip wheel #then: # python2 -m pip install Pillow # sudo pip install Pillow #or #same but python3 instead # sudo pip install Pillow """ try: from PIL import Image, ImageDraw, ImageFont, ImageColor except ImportError: print(PIL_HELP) exit() def join_as_str(delimiter, arr): return delimiter.join(str(x) for x in arr) def pngsave(im, file): """ This is a wrapper around PIL 1.1.6 Image.save to preserve PNG metadata. CC0 Nick Galbreath These can be automatically added to Image.info dict. They are not user-added metadata. """ reserved = ('interlace', 'gamma', 'dpi', 'transparency', 'aspect') # undocumented class try: from PIL import PngImagePlugin except ImportError: print("ERROR: Could not finish at `from PIL import" " PngImagePlugin`") print(PIL_HELP) exit(1) meta = PngImagePlugin.PngInfo() # copy metadata into new object for k, v in im.info.items(): 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 is 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(): # Pre-process geometry so argparse doesn't interpret negative as # an option. geometry = '' i = 0 while i < len(sys.argv): if sys.argv[i] == "--geometry": del sys.argv[i] geometry = None elif geometry is None: geometry = sys.argv[i] del sys.argv[i] else: i += 1 i = None ap = argparse.ArgumentParser(description='A mapper for minetest') ap.add_argument('--bgcolor', default='black', metavar='COLOR', type=ImageColor.getrgb, help=('set the background color (e.g. white or' ' "#FFFFFF")')) ap.add_argument('--scalecolor', default='white', metavar='COLOR', type=ImageColor.getrgb, help='set the ruler and text color for the scale') ap.add_argument('--origincolor', default='red', metavar='COLOR', type=ImageColor.getrgb, help='set the color for the map origin') ap.add_argument('--playercolor', default='red', metavar='COLOR', type=ImageColor.getrgb, help='set the color for player markers') ap.add_argument('--fogcolor', default='grey', metavar='COLOR', type=ImageColor.getrgb, help='set the color for fog (default grey)') ap.add_argument('--ugcolor', default='purple', metavar='COLOR', type=ImageColor.getrgb, help=('set the color for underground areas (default' ' purple)')) ap.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')) ap.add_argument('--drawscale', action='store_const', const=True, default=False, help=('draw a scale on the border of' ' the map')) ap.add_argument('--drawplayers', action='store_const', const=True, default=False, help='draw markers for players') ap.add_argument('--draworigin', action='store_const', const=True, default=False, help=('draw the position of the' ' origin (0,0)')) ap.add_argument('--drawalpha', dest='drawalpha', action='store_const', const=1, default=0, help=('accepted for compatibility but' ' NOT YET IMPLEMENTED in this version')) ap.add_argument('--noshading', dest='noshading', action='store_const', const=1, default=0, help=('accepted for compatibility but' ' NOT YET IMPLEMENTED in this version')) ap.add_argument('--drawunderground', dest='drawunderground', action='store_const', const=1, default=0, help='draw underground areas overlaid on the map') ap.add_argument('--drawunderground-standalone', dest='drawunderground', action='store_const', const=2, help=('draw underground areas as a' ' standalone map')) ap.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)')) ap.add_argument('--max-y', dest='maxheight', type=int, metavar=('YMAX'), default=500, help='don\'t draw above height YMAX (default=500)') ap.add_argument('--min-y', dest='minheight', type=int, metavar=('YMIN'), default=-500, help='don\'t draw below height YMIN (defualt = -500)') ap.add_argument('--zoom', dest='pixelspernode', type=int, metavar=('PPN'), default=1, help='number of pixels per node (default=1)') ap.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)')) ap.add_argument('--geometry', type=geometry_to_ints, default=None, action='store', help=('Specify a region in X:Y+W+H format.')) # ^ handle hyphen as negative number not option # See: # - # - (says to use action='store'--didn't work) ap.add_argument('--scales', type=str, default='', help=('accepted for compatibility but' ' NOT YET IMPLEMENTED in this version')) ap.add_argument('--colors', type=str, default='colors.txt', help=('specify a colors list in colors.txt format' '"80c 183 183 222 # CONTENT_GLASS"' 'or "default:stone 128 128 128")')) ap.add_argument('--backend', type=str, choices=('leveldb', 'sqlite3'), default='leveldb', help=('manually specify leveldb or sqlite3' ' (others are NOT YET IMPLEMENTED)')) ap.add_argument('--fog', type=float, metavar=('FOGSTRENGTH'), default=0.0, help=('use fog strength of' ' FOGSTRENGTH (0.0 by' ' default, max of 1.0)')) ap.add_argument('-i', '--input', help='the path to the world you want to map') ap.add_argument('-o', '--output', nargs="?", default='map.png', help='the output filename') args = ap.parse_args() if args.input is None: print("Please select world path (eg. -i ../worlds/yourworld)" " (or use --help)") sys.exit(1) if not os.path.isdir(args.input): print("World does not exist") sys.exit(1) args.input = os.path.abspath(args.input) args.geometry = geometry return args # Load color information for the blocks. def load_colors(fname="colors.txt"): uid_to_color = {} str_to_uid = {} uid = 2 # unique id; always ignore == 0, air == 1 since 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": ss_path = os.path.join(args.input, "sectors") filename = os.path.join(ss_path, sector_data[0], + yhex.lower()) else: ss2_path = os.path.join(args.input, "sectors2") filename = os.path.join(ss2_path, sector_data[1], yhex.lower()) return open(filename, "rb") def legacy_sector_scan(args, sectors_xmin, sector_xmax, sector_zmin, sector_zmax): ss_path = os.path.join(args.input, "sectors") ss2_path = os.path.join(args.input, "sectors2") if os.path.exists(ss2_path): for filename in os.listdir(ss2_path): ss2_f_path = os.path.join(ss2_path, filename) for filename2 in os.listdir(ss2_f_path): 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) elif os.path.exists(ss_path): for filename in os.listdir(ss_path): 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 = os.path.join(xhex.lower(), zhex.lower()) ss_path = os.path.join(args.input, "sectors") ss2_path = os.path.join(args.input, "sectors2") try: ss_s1_path = ss_path + sector1 for filename in os.listdir(ss_s1_path): if (filename != "meta"): pos = int(filename, 16) if (pos > 32767): pos -= 65536 ylist.append(pos) if len(ylist) > 0: sectortype = "old" if sectortype == "": try: ss2_s2_path = os.path.join(ss2_path, sector2) for filename in os.listdir(ss2_s2_path): 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 = list(range(maxy, -1, -1)) else: r = list(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 = list(range(maxy, -1, -1)) else: r = list(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): backend = args.backend if args.backend is None: # This should never happen. print("* detecting db type from world.mt in " "{}".format(args.input)) if not os.path.exists(os.path.join(args.input, "world.mt")): return None with open(os.path.join(args.input, "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(os.path.join(args.input, "map.sqlite")) if backend == "leveldb": return LVLDB(os.path.join(args.input, "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 # print("getting int from first index of value " + str(r)) 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(): try: val = k[0] x, y, z = getIntegerAsBlock(int(val)) yield x, y, z, val except Exception as e: print("Could not finish getting int from first index of" " value {}".format(k)) raise e def get(self, pos): return BytesIO(self.conn.Get(pos)) R_MSG = ("<(The following issue occurred while handling the exception" " above) Could not finish writing r error since r was not" " initialized>") def geometry_to_ints(s): """Convert a string in the format X:Y+W+H to a 4-part tuple""" chunks = s.split("+") if len(chunks) != 3: raise ValueError("Geometry must be in the format X:Y+W+H" " not {}".format(s)) topleft = chunks[0].split(":") if len(topleft) != 2: raise ValueError("Geometry must be in the format X:Y+W+H" " not {}".format(s)) return (int(topleft[0]), int(topleft[1]), int(chunks[1]), int(chunks[2])) class World: def __init__(self, args): self.r_error_enable = True 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 rect = args.region if len(args.geometry) > 0: rect = geometry_to_ints(args.geometry) ( sector_xmin, sector_xmax, sector_zmin, sector_zmax ) = numpy.array(rect)/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 = list(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(rect) 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.frombuffer(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: 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) # if facing in down, south, west: use maxheight; # else 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 plist == True or ypos==ylist[-1][0]: if ((not args.drawunderground and len(plist) == 0) 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 {}:".format((xpos, ypos, zpos))) traceback.print_exc() sys.stdout.write(os.linesep) sys.stdout.write("Block data: ") try: for c in r[0]: sys.stdout.write("%2.2x " % ord(c)) except NameError: if self.r_error_enable: sys.stdout.write(R_MSG) # stop here or stdout is several hundred MB: self.r_error_enable = False sys.stdout.write(os.linesep) sys.stdout.write("Data after node metadata:") d_a_n_md = data_after_node_metadata try: count = 0 for c in data_after_node_metadata: sys.stdout.write("%2.2x " % ord(c)) count += 1 if count == 0: sys.stdout.write("<(The following issue" " occurred while handling" " the exception above) Uh" " oh, got {}: zero" " characters to convert to" " ord>".format(d_a_n_md)) except TypeError: sys.stdout.write("<(The following issue" " occurred while handling the" " exception above) Uh oh," " expected characters in" " data_after_node_metadata;" " got:") sys.stdout.flush() sys.stdout.write(str(len(d_a_n_md)) + "-length " + type(d_a_n_md).__name__ + ":>") sys.stdout.write(os.linesep) sys.stdout.write(os.linesep) exit(1) # stop HUGE stdout 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 # normalize so that 6 blocks of air underground is considered # "big": ugstrength = 1.0 * (stuff['underground']) / 6 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: # regular 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: # Average the color with background color based on depth (deeper # caves will be closer to the bg color). ugpd = args.ugcolor * ugdepth + args.bgcolor * (1-ugdepth) 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: players_path = os.path.join(args.input, "players") for filename in os.listdir(players_path): f = open(os.path.join(players_path, 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+16 pngminz = minz*16 pngmaxz = maxz*16+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"] = join_as_str(",", pngregion) 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(fname=args.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()