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