* honor environment variables, use utf-8
[oweals/minetest.git] / util / minetestmapper.py
1 #!/usr/bin/env python
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 from PIL import Image, ImageDraw, ImageFont, ImageColor
33
34 CONTENT_WATER = [2, 9]
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 limit(i, l, h):
89     if(i > h):
90         i = h
91     if(i < l):
92         i = l
93     return i
94
95
96 def usage():
97     print "TODO: Help"
98 try:
99     opts, args = getopt.getopt(sys.argv[1:], "hi:o:", ["help", "input=",
100         "output=", "bgcolor=", "scalecolor=", "origincolor=",
101         "playercolor=", "draworigin", "drawplayers", "drawscale"])
102 except getopt.GetoptError, err:
103     # print help information and exit:
104     print str(err)  # will print something like "option -a not recognized"
105     usage()
106     sys.exit(2)
107
108 path = "../world/"
109 output = "map.png"
110 border = 0
111 scalecolor = "black"
112 bgcolor = "white"
113 origincolor = "red"
114 playercolor = "red"
115 drawscale = False
116 drawplayers = False
117 draworigin = False
118
119 sector_xmin = -1500 / 16
120 sector_xmax = 1500 / 16
121 sector_zmin = -1500 / 16
122 sector_zmax = 1500 / 16
123
124 for o, a in opts:
125     if o in ("-h", "--help"):
126         usage()
127         sys.exit()
128     elif o in ("-i", "--input"):
129         path = a
130     elif o in ("-o", "--output"):
131         output = a
132     elif o == "--bgcolor":
133         bgcolor = ImageColor.getrgb(a)
134     elif o == "--scalecolor":
135         scalecolor = ImageColor.getrgb(a)
136     elif o == "--playercolor":
137         playercolor = ImageColor.getrgb(a)
138     elif o == "--origincolor":
139         origincolor = ImageColor.getrgb(a)
140     elif o == "--drawscale":
141         drawscale = True
142         border = 40
143     elif o == "--drawplayers":
144         drawplayers = True
145     elif o == "--draworigin":
146         draworigin = True
147     else:
148         assert False, "unhandled option"
149
150 if path[-1:] != "/" and path[-1:] != "\\":
151     path = path + "/"
152
153 # Load color information for the blocks.
154 colors = {}
155 f = file("colors.txt")
156 for line in f:
157     values = string.split(line)
158     colors[int(values[0], 16)] = (
159         int(values[1]),
160         int(values[2]),
161         int(values[3]))
162 f.close()
163
164 xlist = []
165 zlist = []
166
167 # List all sectors to memory and calculate the width and heigth of the
168 # resulting picture.
169 if os.path.exists(path + "sectors2"):
170     for filename in os.listdir(path + "sectors2"):
171         for filename2 in os.listdir(path + "sectors2/" + filename):
172             x = hex_to_int(filename)
173             z = hex_to_int(filename2)
174             if x < sector_xmin or x > sector_xmax:
175                 continue
176             if z < sector_zmin or z > sector_zmax:
177                 continue
178             xlist.append(x)
179             zlist.append(z)
180
181 if os.path.exists(path + "sectors"):
182     for filename in os.listdir(path + "sectors"):
183         x = hex4_to_int(filename[:4])
184         z = hex4_to_int(filename[-4:])
185         if x < sector_xmin or x > sector_xmax:
186             continue
187         if z < sector_zmin or z > sector_zmax:
188             continue
189         xlist.append(x)
190         zlist.append(z)
191
192 minx = min(xlist)
193 minz = min(zlist)
194 maxx = max(xlist)
195 maxz = max(zlist)
196
197 w = (maxx - minx) * 16 + 16
198 h = (maxz - minz) * 16 + 16
199
200 print "w=" + str(w) + " h=" + str(h)
201
202 im = Image.new("RGB", (w + border, h + border), bgcolor)
203 draw = ImageDraw.Draw(im)
204 impix = im.load()
205
206 stuff = {}
207
208 starttime = time.time()
209
210
211 def data_is_air(d):
212     return d in [126, 127, 254]
213
214
215 def read_blocknum(mapdata, version, datapos):
216     if version == 20:
217         if mapdata[datapos] < 0x80:
218             return mapdata[datapos]
219         else:
220             return (mapdata[datapos] << 4) | (mapdata[datapos + 0x2000] >> 4)
221     elif 16 <= version < 20:
222         return TRANSLATION_TABLE.get(mapdata[datapos], mapdata[datapos])
223     else:
224         raise Exception("Unsupported map format: " + str(version))
225
226
227 def read_mapdata(f, version, pixellist, water):
228     global stuff  # oh my :-)
229
230     dec_o = zlib.decompressobj()
231     try:
232         mapdata = array.array("B", dec_o.decompress(f.read()))
233     except:
234         mapdata = []
235
236     f.close()
237
238     if(len(mapdata) < 4096):
239         print "bad: " + xhex + "/" + zhex + "/" + yhex + " " + \
240             str(len(mapdata))
241     else:
242         chunkxpos = xpos * 16
243         chunkypos = ypos * 16
244         chunkzpos = zpos * 16
245         blocknum = 0
246         datapos = 0
247         for (x, z) in reversed(pixellist):
248             for y in reversed(range(16)):
249                 datapos = x + y * 16 + z * 256
250                 blocknum = read_blocknum(mapdata, version, datapos)
251                 if not data_is_air(blocknum) and blocknum in colors:
252                     if blocknum in CONTENT_WATER:
253                         water[(x, z)] += 1
254                         # Add dummy stuff for drawing sea without seabed
255                         stuff[(chunkxpos + x, chunkzpos + z)] = (
256                             chunkypos + y, blocknum, water[(x, z)])
257                     else:
258                         pixellist.remove((x, z))
259                         # Memorize information on the type and height of
260                         # the block and for drawing the picture.
261                         stuff[(chunkxpos + x, chunkzpos + z)] = (
262                             chunkypos + y, blocknum, water[(x, z)])
263                         break
264                 elif not data_is_air(blocknum) and blocknum not in colors:
265                     print "strange block: %s/%s/%s x: %d y: %d z: %d \
266 block id: %x" % (xhex, zhex, yhex, x, y, z, blocknum)
267
268 # Go through all sectors.
269 for n in range(len(xlist)):
270     #if n > 500:
271     #   break
272     if n % 200 == 0:
273         nowtime = time.time()
274         dtime = nowtime - starttime
275         try:
276             n_per_second = 1.0 * n / dtime
277         except ZeroDivisionError:
278             n_per_second = 0
279         if n_per_second != 0:
280             seconds_per_n = 1.0 / n_per_second
281             time_guess = seconds_per_n * len(xlist)
282             remaining_s = time_guess - dtime
283             remaining_minutes = int(remaining_s / 60)
284             remaining_s -= remaining_minutes * 60
285             print("Processing sector " + str(n) + " of " + str(len(xlist))
286                     + " (" + str(round(100.0 * n / len(xlist), 1)) + "%)"
287                     + " (ETA: " + str(remaining_minutes) + "m "
288                     + str(int(remaining_s)) + "s)")
289
290     xpos = xlist[n]
291     zpos = zlist[n]
292
293     xhex = int_to_hex3(xpos)
294     zhex = int_to_hex3(zpos)
295     xhex4 = int_to_hex4(xpos)
296     zhex4 = int_to_hex4(zpos)
297
298     sector1 = xhex4.lower() + zhex4.lower()
299     sector2 = xhex.lower() + "/" + zhex.lower()
300
301     ylist = []
302
303     sectortype = ""
304
305     try:
306         for filename in os.listdir(path + "sectors/" + sector1):
307             if(filename != "meta"):
308                 pos = int(filename, 16)
309                 if(pos > 32767):
310                     pos -= 65536
311                 ylist.append(pos)
312                 sectortype = "old"
313     except OSError:
314         pass
315
316     if sectortype != "old":
317         try:
318             for filename in os.listdir(path + "sectors2/" + sector2):
319                 if(filename != "meta"):
320                     pos = int(filename, 16)
321                     if(pos > 32767):
322                         pos -= 65536
323                     ylist.append(pos)
324                     sectortype = "new"
325         except OSError:
326             pass
327
328     if sectortype == "":
329         continue
330
331     ylist.sort()
332
333     # Make a list of pixels of the sector that are to be looked for.
334     pixellist = []
335     water = {}
336     for x in range(16):
337         for z in range(16):
338             pixellist.append((x, z))
339             water[(x, z)] = 0
340
341     # Go through the Y axis from top to bottom.
342     ylist2 = []
343     for ypos in reversed(ylist):
344
345         yhex = int_to_hex4(ypos)
346
347         filename = ""
348         if sectortype == "old":
349             filename = path + "sectors/" + sector1 + "/" + yhex.lower()
350         else:
351             filename = path + "sectors2/" + sector2 + "/" + yhex.lower()
352
353         f = file(filename, "rb")
354
355         # Let's just memorize these even though it's not really necessary.
356         version = ord(f.read(1))
357         flags = f.read(1)
358
359         # Checking day and night differs -flag
360         if not ord(flags) & 2:
361             ylist2.append((ypos, filename))
362             f.close()
363             continue
364
365         read_mapdata(f, version, pixellist, water)
366
367         # After finding all the pixels in the sector, we can move on to
368         # the next sector without having to continue the Y axis.
369         if(len(pixellist) == 0):
370             break
371
372     if len(pixellist) > 0:
373         for (ypos, filename) in ylist2:
374             f = file(filename, "rb")
375
376             version = ord(f.read(1))
377             flags = f.read(1)
378
379             read_mapdata(f, version, pixellist, water)
380
381             # After finding all the pixels in the sector, we can move on
382             # to the next sector without having to continue the Y axis.
383             if(len(pixellist) == 0):
384                 break
385
386 print "Drawing image"
387 # Drawing the picture
388 starttime = time.time()
389 n = 0
390 for (x, z) in stuff.iterkeys():
391     if n % 500000 == 0:
392         nowtime = time.time()
393         dtime = nowtime - starttime
394         try:
395             n_per_second = 1.0 * n / dtime
396         except ZeroDivisionError:
397             n_per_second = 0
398         if n_per_second != 0:
399             listlen = len(stuff)
400             seconds_per_n = 1.0 / n_per_second
401             time_guess = seconds_per_n * listlen
402             remaining_s = time_guess - dtime
403             remaining_minutes = int(remaining_s / 60)
404             remaining_s -= remaining_minutes * 60
405             print("Drawing pixel " + str(n) + " of " + str(listlen)
406                     + " (" + str(round(100.0 * n / listlen, 1)) + "%)"
407                     + " (ETA: " + str(remaining_minutes) + "m "
408                     + str(int(remaining_s)) + "s)")
409     n += 1
410
411     (r, g, b) = colors[stuff[(x, z)][1]]
412     # Comparing heights of a couple of adjacent blocks and changing
413     # brightness accordingly.
414     try:
415         c1 = stuff[(x - 1, z)][1]
416         c2 = stuff[(x, z + 1)][1]
417         c = stuff[(x, z)][1]
418         if c1 not in CONTENT_WATER and c2 not in CONTENT_WATER and \
419             c not in CONTENT_WATER:
420             y1 = stuff[(x - 1, z)][0]
421             y2 = stuff[(x, z + 1)][0]
422             y = stuff[(x, z)][0]
423
424             d = ((y - y1) + (y - y2)) * 12
425         else:
426             d = 0
427
428         if(d > 36):
429             d = 36
430
431         r = limit(r + d, 0, 255)
432         g = limit(g + d, 0, 255)
433         b = limit(b + d, 0, 255)
434     except:
435         pass
436
437     # Water
438     if(stuff[(x, z)][2] > 0):
439         r = int(r * .15 + colors[2][0] * .85)
440         g = int(g * .15 + colors[2][1] * .85)
441         b = int(b * .15 + colors[2][2] * .85)
442
443     impix[x - minx * 16 + border, h - 1 - (z - minz * 16) + border] = (r, g, b)
444
445
446 if draworigin:
447     draw.ellipse((minx * -16 - 5 + border, h - minz * -16 - 6 + border,
448         minx * -16 + 5 + border, h - minz * -16 + 4 + border),
449         outline=origincolor)
450
451 font = ImageFont.load_default()
452
453 if drawscale:
454     draw.text((24, 0), "X", font=font, fill=scalecolor)
455     draw.text((2, 24), "Z", font=font, fill=scalecolor)
456
457     for n in range(int(minx / -4) * -4, maxx, 4):
458         draw.text((minx * -16 + n * 16 + 2 + border, 0), str(n * 16),
459             font=font, fill=scalecolor)
460         draw.line((minx * -16 + n * 16 + border, 0,
461             minx * -16 + n * 16 + border, border - 1), fill=scalecolor)
462
463     for n in range(int(maxz / 4) * 4, minz, -4):
464         draw.text((2, h - 1 - (n * 16 - minz * 16) + border), str(n * 16),
465             font=font, fill=scalecolor)
466         draw.line((0, h - 1 - (n * 16 - minz * 16) + border, border - 1,
467             h - 1 - (n * 16 - minz * 16) + border), fill=scalecolor)
468
469 if drawplayers:
470     try:
471         for filename in os.listdir(path + "players"):
472             f = file(path + "players/" + filename)
473             lines = f.readlines()
474             name = ""
475             position = []
476             for line in lines:
477                 p = string.split(line)
478                 if p[0] == "name":
479                     name = p[2]
480                     print filename + ": name = " + name
481                 if p[0] == "position":
482                     position = string.split(p[2][1:-1], ",")
483                     print filename + ": position = " + p[2]
484             if len(name) > 0 and len(position) == 3:
485                 x = (int(float(position[0]) / 10 - minx * 16))
486                 z = int(h - (float(position[2]) / 10 - minz * 16))
487                 draw.ellipse((x - 2 + border, z - 2 + border,
488                     x + 2 + border, z + 2 + border), outline=playercolor)
489                 draw.text((x + 2 + border, z + 2 + border), name,
490                     font=font, fill=playercolor)
491             f.close()
492     except OSError:
493         pass
494
495 print "Saving"
496 im.save(output)