Update minetestmapper.py to support the current map format (and previous ones)

This commit is contained in:
Perttu Ahola 2012-06-08 15:17:03 +03:00
parent e74668ef7f
commit ff85e2343c
2 changed files with 262 additions and 48 deletions

@ -8,8 +8,8 @@ f 128 79 0 # CONTENT_CHEST
15 103 78 42 # CONTENT_FENCE 15 103 78 42 # CONTENT_FENCE
1e 162 119 53 # CONTENT_RAIL 1e 162 119 53 # CONTENT_RAIL
1f 154 110 40 # CONTENT_LADDER 1f 154 110 40 # CONTENT_LADDER
20 255 204 0 # CONTENT_LAVA 20 255 100 0 # CONTENT_LAVA
21 255 204 0 # CONTENT_LAVASOURCE 21 255 100 0 # CONTENT_LAVASOURCE
800 107 134 51 # CONTENT_GRASS 800 107 134 51 # CONTENT_GRASS
801 86 58 31 # CONTENT_TREE 801 86 58 31 # CONTENT_TREE
802 48 95 8 # CONTENT_LEAVES 802 48 95 8 # CONTENT_LEAVES
@ -22,7 +22,7 @@ f 128 79 0 # CONTENT_CHEST
80b 199 199 199 # CONTENT_STEEL 80b 199 199 199 # CONTENT_STEEL
80c 183 183 222 # CONTENT_GLASS 80c 183 183 222 # CONTENT_GLASS
80d 219 202 178 # CONTENT_MOSSYCOBBLE 80d 219 202 178 # CONTENT_MOSSYCOBBLE
80e 78 154 6 # CONTENT_GRAVEL 80e 70 70 70 # CONTENT_GRAVEL
80f 204 0 0 # CONTENT_SANDSTONE 80f 204 0 0 # CONTENT_SANDSTONE
810 0 215 0 # CONTENT_CACTUS 810 0 215 0 # CONTENT_CACTUS
811 170 50 25 # CONTENT_BRICK 811 170 50 25 # CONTENT_BRICK
@ -34,3 +34,45 @@ f 128 79 0 # CONTENT_CHEST
817 255 153 255 # CONTENT_NC 817 255 153 255 # CONTENT_NC
818 102 50 255 # CONTENT_NC_RB 818 102 50 255 # CONTENT_NC_RB
819 200 0 0 # CONTENT_APPLE 819 200 0 0 # CONTENT_APPLE
default:stone 128 128 128
default:stone_with_coal 50 50 50
default:water_flowing 39 66 106
default:torch 255 255 0
default:water_source 39 66 106
default:sign_wall 117 86 41
default:chest 128 79 0
default:furnace 118 118 118
default:fence_wood 103 78 42
default:rail 162 119 53
default:ladder 154 110 40
default:lava_flowing 255 100 0
default:lava_source 255 100 0
default:dirt_with_grass 107 134 51
default:tree 86 58 31
default:leaves 48 95 8
default:dirt_with_grass_and_footsteps 102 129 38
default:mese 178 178 0
default:dirt 101 84 36
default:wood 104 78 42
default:sand 210 194 156
default:cobble 123 123 123
default:steelblock 199 199 199
default:glass 183 183 222
default:mossycobble 219 202 178
default:gravel 70 70 70
default:sandstone 204 0 0
default:cactus 0 215 0
default:brick 170 50 25
default:clay 104 78 42
default:papyrus 58 105 18
default:bookshelf 196 160 0
default:jungletree 205 190 121
default:junglegrass 62 101 25
default:nyancat 255 153 255
default:nyancat_rainbow 102 50 255
default:apple 200 0 0
default:desert_sand 210 180 50
default:desert_stone 150 100 30
default:dry_shrub 100 80 40

262
util/minetestmapper.py Normal file → Executable file

@ -1,4 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python2
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# This program is free software. It comes without any warranty, to # This program is free software. It comes without any warranty, to
@ -30,6 +30,7 @@ import getopt
import sys import sys
import array import array
import cStringIO import cStringIO
import traceback
from PIL import Image, ImageDraw, ImageFont, ImageColor from PIL import Image, ImageDraw, ImageFont, ImageColor
TRANSLATION_TABLE = { TRANSLATION_TABLE = {
@ -108,9 +109,34 @@ def limit(i, l, h):
i = l i = l
return i 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 <world_path>
-o/--output <output_image.png>
--bgcolor <color>
--scalecolor <color>
--playercolor <color>
--origincolor <color>
--drawscale
--drawplayers
--draworigin
--drawunderground
Color format: '#000000'"""
def usage(): def usage():
print("TODO: Help") print(usagetext)
try: try:
opts, args = getopt.getopt(sys.argv[1:], "hi:o:", ["help", "input=", opts, args = getopt.getopt(sys.argv[1:], "hi:o:", ["help", "input=",
"output=", "bgcolor=", "scalecolor=", "origincolor=", "output=", "bgcolor=", "scalecolor=", "origincolor=",
@ -122,7 +148,7 @@ except getopt.GetoptError as err:
usage() usage()
sys.exit(2) sys.exit(2)
path = "../world/" path = None
output = "map.png" output = "map.png"
border = 0 border = 0
scalecolor = "black" scalecolor = "black"
@ -167,6 +193,10 @@ for o, a in opts:
else: else:
assert False, "unhandled option" 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:] != "\\": if path[-1:] != "/" and path[-1:] != "\\":
path = path + "/" path = path + "/"
@ -178,12 +208,29 @@ except IOError:
f = file(os.path.join(os.path.dirname(__file__), "colors.txt")) f = file(os.path.join(os.path.dirname(__file__), "colors.txt"))
for line in f: for line in f:
values = string.split(line) values = string.split(line)
colors[int(values[0], 16)] = ( if len(values) < 4:
int(values[1]), continue
int(values[2]), identifier = values[0]
int(values[3])) 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() f.close()
#print("colors: "+repr(colors))
#sys.exit(1)
xlist = [] xlist = []
zlist = [] zlist = []
@ -236,6 +283,10 @@ if os.path.exists(path + "sectors"):
xlist.append(x) xlist.append(x)
zlist.append(z) zlist.append(z)
if len(xlist) == 0 or len(zlist) == 0:
print("World does not exist.")
sys.exit(1)
# Get rid of doubles # Get rid of doubles
xlist, zlist = zip(*sorted(set(zip(xlist, zlist)))) xlist, zlist = zip(*sorted(set(zip(xlist, zlist))))
@ -247,7 +298,8 @@ maxz = max(zlist)
w = (maxx - minx) * 16 + 16 w = (maxx - minx) * 16 + 16
h = (maxz - minz) * 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) im = Image.new("RGB", (w + border, h + border), bgcolor)
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
@ -255,18 +307,24 @@ impix = im.load()
stuff = {} stuff = {}
unknown_node_names = []
unknown_node_ids = []
starttime = time.time() starttime = time.time()
CONTENT_WATER = 2 CONTENT_WATER = 2
def content_is_ignore(d):
return d in [0, "ignore"]
def content_is_water(d): def content_is_water(d):
return d in [2, 9] return d in [2, 9]
def content_is_air(d): def content_is_air(d):
return d in [126, 127, 254] return d in [126, 127, 254, "air"]
def read_content(mapdata, version, datapos): def read_content(mapdata, version, datapos):
if version == 20: if version >= 20:
if mapdata[datapos] < 0x80: if mapdata[datapos] < 0x80:
return mapdata[datapos] return mapdata[datapos]
else: else:
@ -277,16 +335,10 @@ def read_content(mapdata, version, datapos):
raise Exception("Unsupported map format: " + str(version)) raise Exception("Unsupported map format: " + str(version))
def read_mapdata(f, version, pixellist, water, day_night_differs): def read_mapdata(mapdata, version, pixellist, water, day_night_differs, id_to_name):
global stuff # oh my :-) global stuff # oh my :-)
global unknown_node_names
dec_o = zlib.decompressobj() global unknown_node_ids
try:
mapdata = array.array("B", dec_o.decompress(f.read()))
except:
mapdata = []
f.close()
if(len(mapdata) < 4096): if(len(mapdata) < 4096):
print("bad: " + xhex + "/" + zhex + "/" + yhex + " " + \ print("bad: " + xhex + "/" + zhex + "/" + yhex + " " + \
@ -301,7 +353,15 @@ def read_mapdata(f, version, pixellist, water, day_night_differs):
for y in reversed(range(16)): for y in reversed(range(16)):
datapos = x + y * 16 + z * 256 datapos = x + y * 16 + z * 256
content = read_content(mapdata, version, datapos) content = read_content(mapdata, version, datapos)
if content_is_air(content): # 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 pass
elif content_is_water(content): elif content_is_water(content):
water[(x, z)] += 1 water[(x, z)] += 1
@ -316,8 +376,16 @@ def read_mapdata(f, version, pixellist, water, day_night_differs):
pixellist.remove((x, z)) pixellist.remove((x, z))
break break
else: else:
print("strange block: %s/%s/%s x: %d y: %d z: %d \ if type(content) == str:
block id: %x" % (xhex, zhex, yhex, x, y, z, content)) 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:
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. # Go through all sectors.
for n in range(len(xlist)): for n in range(len(xlist)):
@ -405,36 +473,128 @@ for n in range(len(xlist)):
# Go through the Y axis from top to bottom. # Go through the Y axis from top to bottom.
for ypos in reversed(ylist): for ypos in reversed(ylist):
try:
#print("("+str(xpos)+","+str(ypos)+","+str(zpos)+")")
yhex = int_to_hex4(ypos) yhex = int_to_hex4(ypos)
if sectortype == "sqlite": if sectortype == "sqlite":
ps = getBlockAsInteger((xpos, ypos, zpos)) ps = getBlockAsInteger((xpos, ypos, zpos))
cur.execute("SELECT `data` FROM `blocks` WHERE `pos`==? LIMIT 1", (ps,)) cur.execute("SELECT `data` FROM `blocks` WHERE `pos`==? LIMIT 1", (ps,))
r = cur.fetchone() r = cur.fetchone()
if not r: if not r:
continue continue
f = cStringIO.StringIO(r[0]) f = cStringIO.StringIO(r[0])
else:
if sectortype == "old":
filename = path + "sectors/" + sector1 + "/" + yhex.lower()
else: else:
filename = path + "sectors2/" + sector2 + "/" + yhex.lower() if sectortype == "old":
f = file(filename, "rb") 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. # Let's just memorize these even though it's not really necessary.
version = ord(f.read(1)) version = readU8(f)
flags = f.read(1) flags = f.read(1)
# Checking day and night differs -flag #print("version="+str(version))
day_night_differs = ((ord(flags) & 2) != 0) #print("flags="+str(version))
read_mapdata(f, version, pixellist, water, day_night_differs) # 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)
# After finding all the pixels in the sector, we can move on to #print("is_underground="+str(is_underground))
# the next sector without having to continue the Y axis. #print("day_night_differs="+str(day_night_differs))
if(len(pixellist) == 0): #print("lighting_expired="+str(lighting_expired))
break #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)
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
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
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") print("Drawing image")
# Drawing the picture # Drawing the picture
@ -558,3 +718,15 @@ if drawplayers:
print("Saving") print("Saving")
im.save(output) 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)