lua_api.txt: improve noise documentation. Remove previous eased 3D noise format example
[oweals/minetest.git] / util / minetestmapper.py
1 #!/usr/bin/env python2
2 # -*- coding: utf-8 -*-
3
4 # This program is free software. It comes without any warranty, to
5 # the extent permitted by applicable law. You can redistribute it
6 # and/or modify it under the terms of the Do What The Fuck You Want
7 # To Public License, Version 2, as published by Sam Hocevar. See
8 # COPYING for more details.
9
10 # Made by Jogge, modified by celeron55
11 # 2011-05-29: j0gge: initial release
12 # 2011-05-30: celeron55: simultaneous support for sectors/sectors2, removed
13 # 2011-06-02: j0gge: command line parameters, coordinates, players, ...
14 # 2011-06-04: celeron55: added #!/usr/bin/python2 and converted \r\n to \n
15 #                        to make it easily executable on Linux
16 # 2011-07-30: WF: Support for content types extension, refactoring
17 # 2011-07-30: erlehmann: PEP 8 compliance.
18
19 # Requires Python Imaging Library: http://www.pythonware.com/products/pil/
20
21 # Some speed-up: ...lol, actually it slows it down.
22 #import psyco ; psyco.full()
23 #from psyco.classes import *
24
25 import zlib
26 import os
27 import string
28 import time
29 import getopt
30 import sys
31 import array
32 import cStringIO
33 import traceback
34 from PIL import Image, ImageDraw, ImageFont, ImageColor
35
36 TRANSLATION_TABLE = {
37     1: 0x800,  # CONTENT_GRASS
38     4: 0x801,  # CONTENT_TREE
39     5: 0x802,  # CONTENT_LEAVES
40     6: 0x803,  # CONTENT_GRASS_FOOTSTEPS
41     7: 0x804,  # CONTENT_MESE
42     8: 0x805,  # CONTENT_MUD
43     10: 0x806,  # CONTENT_CLOUD
44     11: 0x807,  # CONTENT_COALSTONE
45     12: 0x808,  # CONTENT_WOOD
46     13: 0x809,  # CONTENT_SAND
47     18: 0x80a,  # CONTENT_COBBLE
48     19: 0x80b,  # CONTENT_STEEL
49     20: 0x80c,  # CONTENT_GLASS
50     22: 0x80d,  # CONTENT_MOSSYCOBBLE
51     23: 0x80e,  # CONTENT_GRAVEL
52     24: 0x80f,  # CONTENT_SANDSTONE
53     25: 0x810,  # CONTENT_CACTUS
54     26: 0x811,  # CONTENT_BRICK
55     27: 0x812,  # CONTENT_CLAY
56     28: 0x813,  # CONTENT_PAPYRUS
57     29: 0x814}  # CONTENT_BOOKSHELF
58
59
60 def hex_to_int(h):
61     i = int(h, 16)
62     if(i > 2047):
63         i -= 4096
64     return i
65
66
67 def hex4_to_int(h):
68     i = int(h, 16)
69     if(i > 32767):
70         i -= 65536
71     return i
72
73
74 def int_to_hex3(i):
75     if(i < 0):
76         return "%03X" % (i + 4096)
77     else:
78         return "%03X" % i
79
80
81 def int_to_hex4(i):
82     if(i < 0):
83         return "%04X" % (i + 65536)
84     else:
85         return "%04X" % i
86
87
88 def getBlockAsInteger(p):
89     return p[2]*16777216 + p[1]*4096 + p[0]
90
91 def unsignedToSigned(i, max_positive):
92     if i < max_positive:
93         return i
94     else:
95         return i - 2*max_positive
96
97 def getIntegerAsBlock(i):
98     x = unsignedToSigned(i % 4096, 2048)
99     i = int((i - x) / 4096)
100     y = unsignedToSigned(i % 4096, 2048)
101     i = int((i - y) / 4096)
102     z = unsignedToSigned(i % 4096, 2048)
103     return x,y,z
104
105 def limit(i, l, h):
106     if(i > h):
107         i = h
108     if(i < l):
109         i = l
110     return i
111
112 def readU8(f):
113     return ord(f.read(1))
114
115 def readU16(f):
116     return ord(f.read(1))*256 + ord(f.read(1))
117
118 def readU32(f):
119     return ord(f.read(1))*256*256*256 + ord(f.read(1))*256*256 + ord(f.read(1))*256 + ord(f.read(1))
120
121 def readS32(f):
122     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)
123
124 usagetext = """minetestmapper.py [options]
125   -i/--input <world_path>
126   -o/--output <output_image.png>
127   --bgcolor <color>
128   --scalecolor <color>
129   --playercolor <color>
130   --origincolor <color>
131   --drawscale
132   --drawplayers
133   --draworigin
134   --drawunderground
135 Color format: '#000000'"""
136
137 def usage():
138     print(usagetext)
139
140 try:
141     opts, args = getopt.getopt(sys.argv[1:], "hi:o:", ["help", "input=",
142         "output=", "bgcolor=", "scalecolor=", "origincolor=",
143         "playercolor=", "draworigin", "drawplayers", "drawscale",
144         "drawunderground"])
145 except getopt.GetoptError as err:
146     # print help information and exit:
147     print(str(err))  # will print something like "option -a not recognized"
148     usage()
149     sys.exit(2)
150
151 path = None
152 output = "map.png"
153 border = 0
154 scalecolor = "black"
155 bgcolor = "white"
156 origincolor = "red"
157 playercolor = "red"
158 drawscale = False
159 drawplayers = False
160 draworigin = False
161 drawunderground = False
162
163 sector_xmin = -1500 / 16
164 sector_xmax = 1500 / 16
165 sector_zmin = -1500 / 16
166 sector_zmax = 1500 / 16
167
168 for o, a in opts:
169     if o in ("-h", "--help"):
170         usage()
171         sys.exit()
172     elif o in ("-i", "--input"):
173         path = a
174     elif o in ("-o", "--output"):
175         output = a
176     elif o == "--bgcolor":
177         bgcolor = ImageColor.getrgb(a)
178     elif o == "--scalecolor":
179         scalecolor = ImageColor.getrgb(a)
180     elif o == "--playercolor":
181         playercolor = ImageColor.getrgb(a)
182     elif o == "--origincolor":
183         origincolor = ImageColor.getrgb(a)
184     elif o == "--drawscale":
185         drawscale = True
186         border = 40
187     elif o == "--drawplayers":
188         drawplayers = True
189     elif o == "--draworigin":
190         draworigin = True
191     elif o == "--drawunderground":
192         drawunderground = True
193     else:
194         assert False, "unhandled option"
195
196 if path is None:
197     print("Please select world path (eg. -i ../worlds/yourworld) (or use --help)")
198     sys.exit(1)
199
200 if path[-1:] != "/" and path[-1:] != "\\":
201     path = path + "/"
202
203 # Load color information for the blocks.
204 colors = {}
205 try:
206     f = file("colors.txt")
207 except IOError:
208     f = file(os.path.join(os.path.dirname(__file__), "colors.txt"))
209 for line in f:
210     values = string.split(line)
211     if len(values) < 4:
212         continue
213     identifier = values[0]
214     is_hex = True
215     for c in identifier:
216         if c not in "0123456789abcdefABCDEF":
217             is_hex = False
218             break
219     if is_hex:
220         colors[int(values[0], 16)] = (
221             int(values[1]),
222             int(values[2]),
223             int(values[3]))
224     else:
225         colors[values[0]] = (
226             int(values[1]),
227             int(values[2]),
228             int(values[3]))
229 f.close()
230
231 #print("colors: "+repr(colors))
232 #sys.exit(1)
233
234 xlist = []
235 zlist = []
236
237 # List all sectors to memory and calculate the width and heigth of the
238 # resulting picture.
239
240 conn = None
241 cur = None
242 if os.path.exists(path + "map.sqlite"):
243     import sqlite3
244     conn = sqlite3.connect(path + "map.sqlite")
245     cur = conn.cursor()
246     
247     cur.execute("SELECT `pos` FROM `blocks`")
248     while True:
249         r = cur.fetchone()
250         if not r:
251             break
252         
253         x, y, z = getIntegerAsBlock(r[0])
254         
255         if x < sector_xmin or x > sector_xmax:
256             continue
257         if z < sector_zmin or z > sector_zmax:
258             continue
259         
260         xlist.append(x)
261         zlist.append(z)
262
263 if os.path.exists(path + "sectors2"):
264     for filename in os.listdir(path + "sectors2"):
265         for filename2 in os.listdir(path + "sectors2/" + filename):
266             x = hex_to_int(filename)
267             z = hex_to_int(filename2)
268             if x < sector_xmin or x > sector_xmax:
269                 continue
270             if z < sector_zmin or z > sector_zmax:
271                 continue
272             xlist.append(x)
273             zlist.append(z)
274
275 if os.path.exists(path + "sectors"):
276     for filename in os.listdir(path + "sectors"):
277         x = hex4_to_int(filename[:4])
278         z = hex4_to_int(filename[-4:])
279         if x < sector_xmin or x > sector_xmax:
280             continue
281         if z < sector_zmin or z > sector_zmax:
282             continue
283         xlist.append(x)
284         zlist.append(z)
285
286 if len(xlist) == 0 or len(zlist) == 0:
287     print("World does not exist.")
288     sys.exit(1)
289
290 # Get rid of doubles
291 xlist, zlist = zip(*sorted(set(zip(xlist, zlist))))
292
293 minx = min(xlist)
294 minz = min(zlist)
295 maxx = max(xlist)
296 maxz = max(zlist)
297
298 w = (maxx - minx) * 16 + 16
299 h = (maxz - minz) * 16 + 16
300
301 print("Result image (w=" + str(w) + " h=" + str(h) + ") will be written to "
302         + output)
303
304 im = Image.new("RGB", (w + border, h + border), bgcolor)
305 draw = ImageDraw.Draw(im)
306 impix = im.load()
307
308 stuff = {}
309
310 unknown_node_names = []
311 unknown_node_ids = []
312
313 starttime = time.time()
314
315 CONTENT_WATER = 2
316
317 def content_is_ignore(d):
318     return d in [0, "ignore"]
319
320 def content_is_water(d):
321     return d in [2, 9]
322
323 def content_is_air(d):
324     return d in [126, 127, 254, "air"]
325
326 def read_content(mapdata, version, datapos):
327     if version >= 24:
328         return (mapdata[datapos*2] << 8) | (mapdata[datapos*2 + 1])
329     elif version >= 20:
330         if mapdata[datapos] < 0x80:
331             return mapdata[datapos]
332         else:
333             return (mapdata[datapos] << 4) | (mapdata[datapos + 0x2000] >> 4)
334     elif 16 <= version < 20:
335         return TRANSLATION_TABLE.get(mapdata[datapos], mapdata[datapos])
336     else:
337         raise Exception("Unsupported map format: " + str(version))
338
339
340 def read_mapdata(mapdata, version, pixellist, water, day_night_differs, id_to_name):
341     global stuff  # oh my :-)
342     global unknown_node_names
343     global unknown_node_ids
344
345     if(len(mapdata) < 4096):
346         print("bad: " + xhex + "/" + zhex + "/" + yhex + " " + \
347             str(len(mapdata)))
348     else:
349         chunkxpos = xpos * 16
350         chunkypos = ypos * 16
351         chunkzpos = zpos * 16
352         content = 0
353         datapos = 0
354         for (x, z) in reversed(pixellist):
355             for y in reversed(range(16)):
356                 datapos = x + y * 16 + z * 256
357                 content = read_content(mapdata, version, datapos)
358                 # Try to convert id to name
359                 try:
360                     content = id_to_name[content]
361                 except KeyError:
362                     pass
363
364                 if content_is_ignore(content):
365                     pass
366                 elif content_is_air(content):
367                     pass
368                 elif content_is_water(content):
369                     water[(x, z)] += 1
370                     # Add dummy stuff for drawing sea without seabed
371                     stuff[(chunkxpos + x, chunkzpos + z)] = (
372                         chunkypos + y, content, water[(x, z)], day_night_differs)
373                 elif content in colors:
374                     # Memorize information on the type and height of
375                     # the block and for drawing the picture.
376                     stuff[(chunkxpos + x, chunkzpos + z)] = (
377                         chunkypos + y, content, water[(x, z)], day_night_differs)
378                     pixellist.remove((x, z))
379                     break
380                 else:
381                     if type(content) == str:
382                         if content not in unknown_node_names:
383                             unknown_node_names.append(content)
384                         #print("unknown node: %s/%s/%s x: %d y: %d z: %d block name: %s"
385                         #        % (xhex, zhex, yhex, x, y, z, content))
386                     else:
387                         if content not in unknown_node_ids:
388                             unknown_node_ids.append(content)
389                         #print("unknown node: %s/%s/%s x: %d y: %d z: %d block id: %x"
390                         #        % (xhex, zhex, yhex, x, y, z, content))
391
392
393 # Go through all sectors.
394 for n in range(len(xlist)):
395     #if n > 500:
396     #   break
397     if n % 200 == 0:
398         nowtime = time.time()
399         dtime = nowtime - starttime
400         try:
401             n_per_second = 1.0 * n / dtime
402         except ZeroDivisionError:
403             n_per_second = 0
404         if n_per_second != 0:
405             seconds_per_n = 1.0 / n_per_second
406             time_guess = seconds_per_n * len(xlist)
407             remaining_s = time_guess - dtime
408             remaining_minutes = int(remaining_s / 60)
409             remaining_s -= remaining_minutes * 60
410             print("Processing sector " + str(n) + " of " + str(len(xlist))
411                     + " (" + str(round(100.0 * n / len(xlist), 1)) + "%)"
412                     + " (ETA: " + str(remaining_minutes) + "m "
413                     + str(int(remaining_s)) + "s)")
414
415     xpos = xlist[n]
416     zpos = zlist[n]
417
418     xhex = int_to_hex3(xpos)
419     zhex = int_to_hex3(zpos)
420     xhex4 = int_to_hex4(xpos)
421     zhex4 = int_to_hex4(zpos)
422
423     sector1 = xhex4.lower() + zhex4.lower()
424     sector2 = xhex.lower() + "/" + zhex.lower()
425
426     ylist = []
427
428     sectortype = ""
429
430     if cur:
431         psmin = getBlockAsInteger((xpos, -2048, zpos))
432         psmax = getBlockAsInteger((xpos, 2047, zpos))
433         cur.execute("SELECT `pos` FROM `blocks` WHERE `pos`>=? AND `pos`<=? AND (`pos` - ?) % 4096 = 0", (psmin, psmax, psmin))
434         while True:
435             r = cur.fetchone()
436             if not r:
437                 break
438             pos = getIntegerAsBlock(r[0])[1]
439             ylist.append(pos)
440             sectortype = "sqlite"
441     try:
442         for filename in os.listdir(path + "sectors/" + sector1):
443             if(filename != "meta"):
444                 pos = int(filename, 16)
445                 if(pos > 32767):
446                     pos -= 65536
447                 ylist.append(pos)
448                 sectortype = "old"
449     except OSError:
450         pass
451
452     if sectortype == "":
453         try:
454             for filename in os.listdir(path + "sectors2/" + sector2):
455                 if(filename != "meta"):
456                     pos = int(filename, 16)
457                     if(pos > 32767):
458                         pos -= 65536
459                     ylist.append(pos)
460                     sectortype = "new"
461         except OSError:
462             pass
463
464     if sectortype == "":
465         continue
466
467     ylist.sort()
468
469     # Make a list of pixels of the sector that are to be looked for.
470     pixellist = []
471     water = {}
472     for x in range(16):
473         for z in range(16):
474             pixellist.append((x, z))
475             water[(x, z)] = 0
476
477     # Go through the Y axis from top to bottom.
478     for ypos in reversed(ylist):
479         try:
480             #print("("+str(xpos)+","+str(ypos)+","+str(zpos)+")")
481
482             yhex = int_to_hex4(ypos)
483
484             if sectortype == "sqlite":
485                 ps = getBlockAsInteger((xpos, ypos, zpos))
486                 cur.execute("SELECT `data` FROM `blocks` WHERE `pos`==? LIMIT 1", (ps,))
487                 r = cur.fetchone()
488                 if not r:
489                     continue
490                 f = cStringIO.StringIO(r[0])
491             else:
492                 if sectortype == "old":
493                     filename = path + "sectors/" + sector1 + "/" + yhex.lower()
494                 else:
495                     filename = path + "sectors2/" + sector2 + "/" + yhex.lower()
496                 f = file(filename, "rb")
497
498             # Let's just memorize these even though it's not really necessary.
499             version = readU8(f)
500             flags = f.read(1)
501             
502             #print("version="+str(version))
503             #print("flags="+str(version))
504
505             # Check flags
506             is_underground = ((ord(flags) & 1) != 0)
507             day_night_differs = ((ord(flags) & 2) != 0)
508             lighting_expired = ((ord(flags) & 4) != 0)
509             generated = ((ord(flags) & 8) != 0)
510             
511             #print("is_underground="+str(is_underground))
512             #print("day_night_differs="+str(day_night_differs))
513             #print("lighting_expired="+str(lighting_expired))
514             #print("generated="+str(generated))
515             
516             if version >= 22:
517                 content_width = readU8(f)
518                 params_width = readU8(f)
519
520             # Node data
521             dec_o = zlib.decompressobj()
522             try:
523                 mapdata = array.array("B", dec_o.decompress(f.read()))
524             except:
525                 mapdata = []
526             
527             # Reuse the unused tail of the file
528             f.close();
529             f = cStringIO.StringIO(dec_o.unused_data)
530             #print("unused data: "+repr(dec_o.unused_data))
531
532             # zlib-compressed node metadata list
533             dec_o = zlib.decompressobj()
534             try:
535                 metaliststr = array.array("B", dec_o.decompress(f.read()))
536                 # And do nothing with it
537             except:
538                 metaliststr = []
539             
540             # Reuse the unused tail of the file
541             f.close();
542             f = cStringIO.StringIO(dec_o.unused_data)
543             #print("* dec_o.unused_data: "+repr(dec_o.unused_data))
544             data_after_node_metadata = dec_o.unused_data
545
546             if version <= 21:
547                 # mapblockobject_count
548                 readU16(f)
549
550             if version == 23:
551                 readU8(f) # Unused node timer version (always 0)
552             if version == 24:
553                 ver = readU8(f)
554                 if ver == 1:
555                     num = readU16(f)
556                     for i in range(0,num):
557                         readU16(f)
558                         readS32(f)
559                         readS32(f)
560
561             static_object_version = readU8(f)
562             static_object_count = readU16(f)
563             for i in range(0, static_object_count):
564                 # u8 type (object type-id)
565                 object_type = readU8(f)
566                 # s32 pos_x_nodes * 10000
567                 pos_x_nodes = readS32(f)/10000
568                 # s32 pos_y_nodes * 10000
569                 pos_y_nodes = readS32(f)/10000
570                 # s32 pos_z_nodes * 10000
571                 pos_z_nodes = readS32(f)/10000
572                 # u16 data_size
573                 data_size = readU16(f)
574                 # u8[data_size] data
575                 data = f.read(data_size)
576             
577             timestamp = readU32(f)
578             #print("* timestamp="+str(timestamp))
579             
580             id_to_name = {}
581             if version >= 22:
582                 name_id_mapping_version = readU8(f)
583                 num_name_id_mappings = readU16(f)
584                 #print("* num_name_id_mappings: "+str(num_name_id_mappings))
585                 for i in range(0, num_name_id_mappings):
586                     node_id = readU16(f)
587                     name_len = readU16(f)
588                     name = f.read(name_len)
589                     #print(str(node_id)+" = "+name)
590                     id_to_name[node_id] = name
591
592             # Node timers
593             if version >= 25:
594                 timer_size = readU8(f)
595                 num = readU16(f)
596                 for i in range(0,num):
597                     readU16(f)
598                     readS32(f)
599                     readS32(f)
600
601             read_mapdata(mapdata, version, pixellist, water, day_night_differs, id_to_name)
602
603             # After finding all the pixels in the sector, we can move on to
604             # the next sector without having to continue the Y axis.
605             if(len(pixellist) == 0):
606                 break
607         except Exception as e:
608             print("Error at ("+str(xpos)+","+str(ypos)+","+str(zpos)+"): "+str(e))
609             sys.stdout.write("Block data: ")
610             for c in r[0]:
611                 sys.stdout.write("%2.2x "%ord(c))
612             sys.stdout.write(os.linesep)
613             sys.stdout.write("Data after node metadata: ")
614             for c in data_after_node_metadata:
615                 sys.stdout.write("%2.2x "%ord(c))
616             sys.stdout.write(os.linesep)
617             traceback.print_exc()
618
619 print("Drawing image")
620 # Drawing the picture
621 starttime = time.time()
622 n = 0
623 for (x, z) in stuff.iterkeys():
624     if n % 500000 == 0:
625         nowtime = time.time()
626         dtime = nowtime - starttime
627         try:
628             n_per_second = 1.0 * n / dtime
629         except ZeroDivisionError:
630             n_per_second = 0
631         if n_per_second != 0:
632             listlen = len(stuff)
633             seconds_per_n = 1.0 / n_per_second
634             time_guess = seconds_per_n * listlen
635             remaining_s = time_guess - dtime
636             remaining_minutes = int(remaining_s / 60)
637             remaining_s -= remaining_minutes * 60
638             print("Drawing pixel " + str(n) + " of " + str(listlen)
639                     + " (" + str(round(100.0 * n / listlen, 1)) + "%)"
640                     + " (ETA: " + str(remaining_minutes) + "m "
641                     + str(int(remaining_s)) + "s)")
642     n += 1
643
644     (r, g, b) = colors[stuff[(x, z)][1]]
645
646     dnd = stuff[(x, z)][3]  # day/night differs?
647     if not dnd and not drawunderground:
648         if stuff[(x, z)][2] > 0:  # water
649             (r, g, b) = colors[CONTENT_WATER]
650         else:
651             continue
652
653     # Comparing heights of a couple of adjacent blocks and changing
654     # brightness accordingly.
655     try:
656         c = stuff[(x, z)][1]
657         c1 = stuff[(x - 1, z)][1]
658         c2 = stuff[(x, z + 1)][1]
659         dnd1 = stuff[(x - 1, z)][3]
660         dnd2 = stuff[(x, z + 1)][3]
661         if not dnd:
662             d = -69
663         elif not content_is_water(c1) and not content_is_water(c2) and \
664             not content_is_water(c):
665             y = stuff[(x, z)][0]
666             y1 = stuff[(x - 1, z)][0] if dnd1 else y
667             y2 = stuff[(x, z + 1)][0] if dnd2 else y
668             d = ((y - y1) + (y - y2)) * 12
669         else:
670             d = 0
671
672         if(d > 36):
673             d = 36
674
675         r = limit(r + d, 0, 255)
676         g = limit(g + d, 0, 255)
677         b = limit(b + d, 0, 255)
678     except:
679         pass
680
681     # Water
682     if(stuff[(x, z)][2] > 0):
683         r = int(r * .15 + colors[2][0] * .85)
684         g = int(g * .15 + colors[2][1] * .85)
685         b = int(b * .15 + colors[2][2] * .85)
686
687     impix[x - minx * 16 + border, h - 1 - (z - minz * 16) + border] = (r, g, b)
688
689
690 if draworigin:
691     draw.ellipse((minx * -16 - 5 + border, h - minz * -16 - 6 + border,
692         minx * -16 + 5 + border, h - minz * -16 + 4 + border),
693         outline=origincolor)
694
695 font = ImageFont.load_default()
696
697 if drawscale:
698     draw.text((24, 0), "X", font=font, fill=scalecolor)
699     draw.text((2, 24), "Z", font=font, fill=scalecolor)
700
701     for n in range(int(minx / -4) * -4, maxx, 4):
702         draw.text((minx * -16 + n * 16 + 2 + border, 0), str(n * 16),
703             font=font, fill=scalecolor)
704         draw.line((minx * -16 + n * 16 + border, 0,
705             minx * -16 + n * 16 + border, border - 1), fill=scalecolor)
706
707     for n in range(int(maxz / 4) * 4, minz, -4):
708         draw.text((2, h - 1 - (n * 16 - minz * 16) + border), str(n * 16),
709             font=font, fill=scalecolor)
710         draw.line((0, h - 1 - (n * 16 - minz * 16) + border, border - 1,
711             h - 1 - (n * 16 - minz * 16) + border), fill=scalecolor)
712
713 if drawplayers:
714     try:
715         for filename in os.listdir(path + "players"):
716             f = file(path + "players/" + filename)
717             lines = f.readlines()
718             name = ""
719             position = []
720             for line in lines:
721                 p = string.split(line)
722                 if p[0] == "name":
723                     name = p[2]
724                     print(filename + ": name = " + name)
725                 if p[0] == "position":
726                     position = string.split(p[2][1:-1], ",")
727                     print(filename + ": position = " + p[2])
728             if len(name) > 0 and len(position) == 3:
729                 x = (int(float(position[0]) / 10 - minx * 16))
730                 z = int(h - (float(position[2]) / 10 - minz * 16))
731                 draw.ellipse((x - 2 + border, z - 2 + border,
732                     x + 2 + border, z + 2 + border), outline=playercolor)
733                 draw.text((x + 2 + border, z + 2 + border), name,
734                     font=font, fill=playercolor)
735             f.close()
736     except OSError:
737         pass
738
739 print("Saving")
740 im.save(output)
741
742 if unknown_node_names:
743     sys.stdout.write("Unknown node names:")
744     for name in unknown_node_names:
745         sys.stdout.write(" "+name)
746     sys.stdout.write(os.linesep)
747 if unknown_node_ids:
748     sys.stdout.write("Unknown node ids:")
749     for node_id in unknown_node_ids:
750         sys.stdout.write(" "+str(hex(node_id)))
751     sys.stdout.write(os.linesep)
752