X-Git-Url: https://git.lizzy.rs/?a=blobdiff_plain;f=util%2Fminetestmapper.py;h=9d364e1e64dacc7de1ac0d476162ff99a97e6c4c;hb=a55c073ce9bd78a6ebbb867ab4db2ec611eb66cf;hp=5b175512c4bb3ead8950ca06fe3a1fdc03afe869;hpb=4681392bace0f72bdbff21c2bb805e04e010e43f;p=minetest.git diff --git a/util/minetestmapper.py b/util/minetestmapper.py index 5b175512c..9d364e1e6 100755 --- a/util/minetestmapper.py +++ b/util/minetestmapper.py @@ -1,5 +1,5 @@ -#!/usr/bin/python2 -# -*- coding: windows-1252 -*- +#!/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 @@ -29,10 +29,10 @@ import time import getopt import sys import array +import cStringIO +import traceback from PIL import Image, ImageDraw, ImageFont, ImageColor -CONTENT_WATER = [2, 9] - TRANSLATION_TABLE = { 1: 0x800, # CONTENT_GRASS 4: 0x801, # CONTENT_TREE @@ -85,6 +85,23 @@ def int_to_hex4(i): return "%04X" % i +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 limit(i, l, h): if(i > h): i = h @@ -92,20 +109,46 @@ def limit(i, l, h): i = l return i +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) + +usagetext = """minetestmapper.py [options] + -i/--input + -o/--output + --bgcolor + --scalecolor + --playercolor + --origincolor + --drawscale + --drawplayers + --draworigin + --drawunderground +Color format: '#000000'""" def usage(): - print "TODO: Help" + print(usagetext) + try: opts, args = getopt.getopt(sys.argv[1:], "hi:o:", ["help", "input=", "output=", "bgcolor=", "scalecolor=", "origincolor=", - "playercolor=", "draworigin", "drawplayers", "drawscale"]) -except getopt.GetoptError, err: + "playercolor=", "draworigin", "drawplayers", "drawscale", + "drawunderground"]) +except getopt.GetoptError as err: # print help information and exit: - print str(err) # will print something like "option -a not recognized" + print(str(err)) # will print something like "option -a not recognized" usage() sys.exit(2) -path = "../world/" +path = None output = "map.png" border = 0 scalecolor = "black" @@ -115,6 +158,7 @@ playercolor = "red" drawscale = False drawplayers = False draworigin = False +drawunderground = False sector_xmin = -1500 / 16 sector_xmax = 1500 / 16 @@ -144,28 +188,78 @@ for o, a in opts: drawplayers = True elif o == "--draworigin": draworigin = True + elif o == "--drawunderground": + drawunderground = True else: assert False, "unhandled option" +if path is None: + print("Please select world path (eg. -i ../worlds/yourworld) (or use --help)") + sys.exit(1) + if path[-1:] != "/" and path[-1:] != "\\": path = path + "/" # Load color information for the blocks. colors = {} -f = file("colors.txt") +try: + f = file("colors.txt") +except IOError: + f = file(os.path.join(os.path.dirname(__file__), "colors.txt")) for line in f: values = string.split(line) - colors[int(values[0], 16)] = ( - int(values[1]), - int(values[2]), - int(values[3])) + 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: + colors[int(values[0], 16)] = ( + int(values[1]), + int(values[2]), + int(values[3])) + else: + colors[values[0]] = ( + int(values[1]), + int(values[2]), + int(values[3])) f.close() +#print("colors: "+repr(colors)) +#sys.exit(1) + xlist = [] zlist = [] # List all sectors to memory and calculate the width and heigth of the # resulting picture. + +conn = None +cur = None +if os.path.exists(path + "map.sqlite"): + import sqlite3 + conn = sqlite3.connect(path + "map.sqlite") + cur = conn.cursor() + + cur.execute("SELECT `pos` FROM `blocks`") + while True: + r = cur.fetchone() + if not r: + break + + x, y, z = getIntegerAsBlock(r[0]) + + 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(path + "sectors2"): for filename in os.listdir(path + "sectors2"): for filename2 in os.listdir(path + "sectors2/" + filename): @@ -189,6 +283,13 @@ if os.path.exists(path + "sectors"): xlist.append(x) zlist.append(z) +if len(xlist) == 0 or len(zlist) == 0: + print("World does not exist.") + sys.exit(1) + +# Get rid of doubles +xlist, zlist = zip(*sorted(set(zip(xlist, zlist)))) + minx = min(xlist) minz = min(zlist) maxx = max(xlist) @@ -197,7 +298,8 @@ maxz = max(zlist) w = (maxx - minx) * 16 + 16 h = (maxz - minz) * 16 + 16 -print "w=" + str(w) + " h=" + str(h) +print("Result image (w=" + str(w) + " h=" + str(h) + ") will be written to " + + output) im = Image.new("RGB", (w + border, h + border), bgcolor) draw = ImageDraw.Draw(im) @@ -205,15 +307,26 @@ impix = im.load() stuff = {} +unknown_node_names = [] +unknown_node_ids = [] + starttime = time.time() +CONTENT_WATER = 2 -def data_is_air(d): - return d in [126, 127, 254] +def content_is_ignore(d): + return d in [0, "ignore"] +def content_is_water(d): + return d in [2, 9] -def read_blocknum(mapdata, version, datapos): - if version == 20: +def content_is_air(d): + return d in [126, 127, 254, "air"] + +def read_content(mapdata, version, datapos): + if version >= 24: + return (mapdata[datapos*2] << 8) | (mapdata[datapos*2 + 1]) + elif version >= 20: if mapdata[datapos] < 0x80: return mapdata[datapos] else: @@ -224,46 +337,58 @@ def read_blocknum(mapdata, version, datapos): raise Exception("Unsupported map format: " + str(version)) -def read_mapdata(f, version, pixellist, water): +def read_mapdata(mapdata, version, pixellist, water, day_night_differs, id_to_name): global stuff # oh my :-) - - dec_o = zlib.decompressobj() - try: - mapdata = array.array("B", dec_o.decompress(f.read())) - except: - mapdata = [] - - f.close() + global unknown_node_names + global unknown_node_ids if(len(mapdata) < 4096): - print "bad: " + xhex + "/" + zhex + "/" + yhex + " " + \ - str(len(mapdata)) + print("bad: " + xhex + "/" + zhex + "/" + yhex + " " + \ + str(len(mapdata))) else: chunkxpos = xpos * 16 chunkypos = ypos * 16 chunkzpos = zpos * 16 - blocknum = 0 + content = 0 datapos = 0 for (x, z) in reversed(pixellist): for y in reversed(range(16)): datapos = x + y * 16 + z * 256 - blocknum = read_blocknum(mapdata, version, datapos) - if not data_is_air(blocknum) and blocknum in colors: - if blocknum in CONTENT_WATER: - water[(x, z)] += 1 - # Add dummy stuff for drawing sea without seabed - stuff[(chunkxpos + x, chunkzpos + z)] = ( - chunkypos + y, blocknum, water[(x, z)]) + content = read_content(mapdata, version, datapos) + # Try to convert id to name + try: + content = id_to_name[content] + except KeyError: + pass + + if content_is_ignore(content): + pass + elif content_is_air(content): + pass + elif content_is_water(content): + water[(x, z)] += 1 + # Add dummy stuff for drawing sea without seabed + stuff[(chunkxpos + x, chunkzpos + z)] = ( + chunkypos + y, content, water[(x, z)], day_night_differs) + elif content in colors: + # Memorize information on the type and height of + # the block and for drawing the picture. + stuff[(chunkxpos + x, chunkzpos + z)] = ( + chunkypos + y, content, water[(x, z)], day_night_differs) + pixellist.remove((x, z)) + break + else: + if type(content) == str: + if content not in unknown_node_names: + unknown_node_names.append(content) + #print("unknown node: %s/%s/%s x: %d y: %d z: %d block name: %s" + # % (xhex, zhex, yhex, x, y, z, content)) else: - pixellist.remove((x, z)) - # Memorize information on the type and height of - # the block and for drawing the picture. - stuff[(chunkxpos + x, chunkzpos + z)] = ( - chunkypos + y, blocknum, water[(x, z)]) - break - elif not data_is_air(blocknum) and blocknum not in colors: - print "strange block: %s/%s/%s x: %d y: %d z: %d \ -block id: %x" % (xhex, zhex, yhex, x, y, z, blocknum) + if content not in unknown_node_ids: + unknown_node_ids.append(content) + #print("unknown node: %s/%s/%s x: %d y: %d z: %d block id: %x" + # % (xhex, zhex, yhex, x, y, z, content)) + # Go through all sectors. for n in range(len(xlist)): @@ -302,6 +427,17 @@ for n in range(len(xlist)): sectortype = "" + if cur: + psmin = getBlockAsInteger((xpos, -2048, zpos)) + psmax = getBlockAsInteger((xpos, 2047, zpos)) + cur.execute("SELECT `pos` FROM `blocks` WHERE `pos`>=? AND `pos`<=? AND (`pos` - ?) % 4096 = 0", (psmin, psmax, psmin)) + while True: + r = cur.fetchone() + if not r: + break + pos = getIntegerAsBlock(r[0])[1] + ylist.append(pos) + sectortype = "sqlite" try: for filename in os.listdir(path + "sectors/" + sector1): if(filename != "meta"): @@ -313,7 +449,7 @@ for n in range(len(xlist)): except OSError: pass - if sectortype != "old": + if sectortype == "": try: for filename in os.listdir(path + "sectors2/" + sector2): if(filename != "meta"): @@ -339,51 +475,148 @@ for n in range(len(xlist)): water[(x, z)] = 0 # Go through the Y axis from top to bottom. - ylist2 = [] for ypos in reversed(ylist): - - yhex = int_to_hex4(ypos) - - filename = "" - if sectortype == "old": - filename = path + "sectors/" + sector1 + "/" + yhex.lower() - else: - filename = path + "sectors2/" + sector2 + "/" + yhex.lower() - - f = file(filename, "rb") - - # Let's just memorize these even though it's not really necessary. - version = ord(f.read(1)) - flags = f.read(1) - - # Checking day and night differs -flag - if not ord(flags) & 2: - ylist2.append((ypos, filename)) - f.close() - continue - - read_mapdata(f, version, pixellist, water) - - # After finding all the pixels in the sector, we can move on to - # the next sector without having to continue the Y axis. - if(len(pixellist) == 0): - break - - if len(pixellist) > 0: - for (ypos, filename) in ylist2: - f = file(filename, "rb") - - version = ord(f.read(1)) + try: + #print("("+str(xpos)+","+str(ypos)+","+str(zpos)+")") + + yhex = int_to_hex4(ypos) + + if sectortype == "sqlite": + ps = getBlockAsInteger((xpos, ypos, zpos)) + cur.execute("SELECT `data` FROM `blocks` WHERE `pos`==? LIMIT 1", (ps,)) + r = cur.fetchone() + if not r: + continue + f = cStringIO.StringIO(r[0]) + else: + if sectortype == "old": + filename = path + "sectors/" + sector1 + "/" + yhex.lower() + else: + filename = path + "sectors2/" + sector2 + "/" + yhex.lower() + f = file(filename, "rb") + + # Let's just memorize these even though it's not really necessary. + version = readU8(f) flags = f.read(1) - - read_mapdata(f, version, pixellist, water) - - # After finding all the pixels in the sector, we can move on - # to the next sector without having to continue the Y axis. + + #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: + mapdata = array.array("B", dec_o.decompress(f.read())) + except: + mapdata = [] + + # Reuse the unused tail of the file + f.close(); + f = cStringIO.StringIO(dec_o.unused_data) + #print("unused data: "+repr(dec_o.unused_data)) + + # zlib-compressed node metadata list + dec_o = zlib.decompressobj() + try: + metaliststr = array.array("B", dec_o.decompress(f.read())) + # And do nothing with it + except: + metaliststr = [] + + # Reuse the unused tail of the file + f.close(); + f = cStringIO.StringIO(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 = {} + 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) + #print(str(node_id)+" = "+name) + id_to_name[node_id] = name + + # Node timers + if version >= 25: + timer_size = readU8(f) + num = readU16(f) + for i in range(0,num): + readU16(f) + readS32(f) + readS32(f) + + read_mapdata(mapdata, version, pixellist, water, day_night_differs, id_to_name) + + # After finding all the pixels in the sector, we can move on to + # the next sector without having to continue the Y axis. if(len(pixellist) == 0): break - -print "Drawing image" + except Exception as e: + print("Error at ("+str(xpos)+","+str(ypos)+","+str(zpos)+"): "+str(e)) + 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() + +print("Drawing image") # Drawing the picture starttime = time.time() n = 0 @@ -409,18 +642,29 @@ for (x, z) in stuff.iterkeys(): n += 1 (r, g, b) = colors[stuff[(x, z)][1]] + + dnd = stuff[(x, z)][3] # day/night differs? + if not dnd and not drawunderground: + if stuff[(x, z)][2] > 0: # water + (r, g, b) = colors[CONTENT_WATER] + else: + continue + # Comparing heights of a couple of adjacent blocks and changing # brightness accordingly. try: + c = stuff[(x, z)][1] c1 = stuff[(x - 1, z)][1] c2 = stuff[(x, z + 1)][1] - c = stuff[(x, z)][1] - if c1 not in CONTENT_WATER and c2 not in CONTENT_WATER and \ - c not in CONTENT_WATER: - y1 = stuff[(x - 1, z)][0] - y2 = stuff[(x, z + 1)][0] + dnd1 = stuff[(x - 1, z)][3] + dnd2 = stuff[(x, z + 1)][3] + if not dnd: + d = -69 + elif not content_is_water(c1) and not content_is_water(c2) and \ + not content_is_water(c): y = stuff[(x, z)][0] - + y1 = stuff[(x - 1, z)][0] if dnd1 else y + y2 = stuff[(x, z + 1)][0] if dnd2 else y d = ((y - y1) + (y - y2)) * 12 else: d = 0 @@ -477,10 +721,10 @@ if drawplayers: p = string.split(line) if p[0] == "name": name = p[2] - print filename + ": name = " + name + print(filename + ": name = " + name) if p[0] == "position": position = string.split(p[2][1:-1], ",") - print filename + ": position = " + p[2] + print(filename + ": position = " + p[2]) if len(name) > 0 and len(position) == 3: x = (int(float(position[0]) / 10 - minx * 16)) z = int(h - (float(position[2]) / 10 - minz * 16)) @@ -492,5 +736,17 @@ if drawplayers: except OSError: pass -print "Saving" +print("Saving") im.save(output) + +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) +