minetestmapper speed tweaks (kahrl & JacobF)
[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 class Bytestream:
60     def __init__(self, stream):
61         self.stream = stream
62         self.pos = 0
63         
64         # So you can use files also
65         if hasattr(self.stream, 'read'):
66             self.read = self.stream.read
67     
68     def __len__(self):
69         return len(self.stream)
70     
71     def read(self, length = None):
72         if length is None:
73             length = len(self)
74         self.pos += length
75         return self.stream[self.pos - length:self.pos]
76     
77     def close(self): pass
78
79 def hex_to_int(h):
80     i = int(h, 16)
81     if(i > 2047):
82         i -= 4096
83     return i
84
85
86 def hex4_to_int(h):
87     i = int(h, 16)
88     if(i > 32767):
89         i -= 65536
90     return i
91
92
93 def int_to_hex3(i):
94     if(i < 0):
95         return "%03X" % (i + 4096)
96     else:
97         return "%03X" % i
98
99
100 def int_to_hex4(i):
101     if(i < 0):
102         return "%04X" % (i + 65536)
103     else:
104         return "%04X" % i
105
106
107 def getBlockAsInteger(p):
108     return p[2]*16777216 + p[1]*4096 + p[0]
109
110 def unsignedToSigned(i, max_positive):
111     if i < max_positive:
112         return i
113     else:
114         return i - 2*max_positive
115
116 def getIntegerAsBlock(i):
117     x = unsignedToSigned(i % 4096, 2048)
118     i = int((i - x) / 4096)
119     y = unsignedToSigned(i % 4096, 2048)
120     i = int((i - y) / 4096)
121     z = unsignedToSigned(i % 4096, 2048)
122     return x,y,z
123
124 def limit(i, l, h):
125     if(i > h):
126         i = h
127     if(i < l):
128         i = l
129     return i
130
131
132 def usage():
133     print "TODO: Help"
134 try:
135     opts, args = getopt.getopt(sys.argv[1:], "hi:o:", ["help", "input=",
136         "output=", "bgcolor=", "scalecolor=", "origincolor=",
137         "playercolor=", "draworigin", "drawplayers", "drawscale"])
138 except getopt.GetoptError, err:
139     # print help information and exit:
140     print str(err)  # will print something like "option -a not recognized"
141     usage()
142     sys.exit(2)
143
144 path = "../world/"
145 output = "map.png"
146 border = 0
147 scalecolor = "black"
148 bgcolor = "white"
149 origincolor = "red"
150 playercolor = "red"
151 drawscale = False
152 drawplayers = False
153 draworigin = False
154
155 sector_xmin = -1500 / 16
156 sector_xmax = 1500 / 16
157 sector_zmin = -1500 / 16
158 sector_zmax = 1500 / 16
159
160 for o, a in opts:
161     if o in ("-h", "--help"):
162         usage()
163         sys.exit()
164     elif o in ("-i", "--input"):
165         path = a
166     elif o in ("-o", "--output"):
167         output = a
168     elif o == "--bgcolor":
169         bgcolor = ImageColor.getrgb(a)
170     elif o == "--scalecolor":
171         scalecolor = ImageColor.getrgb(a)
172     elif o == "--playercolor":
173         playercolor = ImageColor.getrgb(a)
174     elif o == "--origincolor":
175         origincolor = ImageColor.getrgb(a)
176     elif o == "--drawscale":
177         drawscale = True
178         border = 40
179     elif o == "--drawplayers":
180         drawplayers = True
181     elif o == "--draworigin":
182         draworigin = True
183     else:
184         assert False, "unhandled option"
185
186 if path[-1:] != "/" and path[-1:] != "\\":
187     path = path + "/"
188
189 # Load color information for the blocks.
190 colors = {}
191 try:
192     f = file("colors.txt")
193 except IOError:
194     f = file(os.path.join(os.path.dirname(__file__), "colors.txt"))
195 for line in f:
196     values = string.split(line)
197     colors[int(values[0], 16)] = (
198         int(values[1]),
199         int(values[2]),
200         int(values[3]))
201 f.close()
202
203 xlist = []
204 zlist = []
205
206 # List all sectors to memory and calculate the width and heigth of the
207 # resulting picture.
208
209 conn = None
210 cur = None
211 if os.path.exists(path + "map.sqlite"):
212     import sqlite3
213     conn = sqlite3.connect(path + "map.sqlite")
214     cur = conn.cursor()
215     
216     cur.execute("SELECT `pos` FROM `blocks`")
217     while True:
218         r = cur.fetchone()
219         if not r:
220             break
221         
222         x, y, z = getIntegerAsBlock(r[0])
223         
224         if x < sector_xmin or x > sector_xmax:
225             continue
226         if z < sector_zmin or z > sector_zmax:
227             continue
228         
229         xlist.append(x)
230         zlist.append(z)
231
232 if os.path.exists(path + "sectors2"):
233     for filename in os.listdir(path + "sectors2"):
234         for filename2 in os.listdir(path + "sectors2/" + filename):
235             x = hex_to_int(filename)
236             z = hex_to_int(filename2)
237             if x < sector_xmin or x > sector_xmax:
238                 continue
239             if z < sector_zmin or z > sector_zmax:
240                 continue
241             xlist.append(x)
242             zlist.append(z)
243
244 if os.path.exists(path + "sectors"):
245     for filename in os.listdir(path + "sectors"):
246         x = hex4_to_int(filename[:4])
247         z = hex4_to_int(filename[-4:])
248         if x < sector_xmin or x > sector_xmax:
249             continue
250         if z < sector_zmin or z > sector_zmax:
251             continue
252         xlist.append(x)
253         zlist.append(z)
254
255 # Get rid of doubles
256 xlist, zlist = zip(*sorted(set(zip(xlist, zlist))))
257
258 minx = min(xlist)
259 minz = min(zlist)
260 maxx = max(xlist)
261 maxz = max(zlist)
262
263 w = (maxx - minx) * 16 + 16
264 h = (maxz - minz) * 16 + 16
265
266 print "w=" + str(w) + " h=" + str(h)
267
268 im = Image.new("RGB", (w + border, h + border), bgcolor)
269 draw = ImageDraw.Draw(im)
270 impix = im.load()
271
272 stuff = {}
273
274 starttime = time.time()
275
276
277 def data_is_air(d):
278     return d in [126, 127, 254]
279
280
281 def read_blocknum(mapdata, version, datapos):
282     if version == 20:
283         if mapdata[datapos] < 0x80:
284             return mapdata[datapos]
285         else:
286             return (mapdata[datapos] << 4) | (mapdata[datapos + 0x2000] >> 4)
287     elif 16 <= version < 20:
288         return TRANSLATION_TABLE.get(mapdata[datapos], mapdata[datapos])
289     else:
290         raise Exception("Unsupported map format: " + str(version))
291
292
293 def read_mapdata(f, version, pixellist, water):
294     global stuff  # oh my :-)
295
296     dec_o = zlib.decompressobj()
297     try:
298         mapdata = array.array("B", dec_o.decompress(f.read()))
299     except:
300         mapdata = []
301
302     f.close()
303
304     if(len(mapdata) < 4096):
305         print "bad: " + xhex + "/" + zhex + "/" + yhex + " " + \
306             str(len(mapdata))
307     else:
308         chunkxpos = xpos * 16
309         chunkypos = ypos * 16
310         chunkzpos = zpos * 16
311         blocknum = 0
312         datapos = 0
313         for (x, z) in reversed(pixellist):
314             for y in reversed(range(16)):
315                 datapos = x + y * 16 + z * 256
316                 blocknum = read_blocknum(mapdata, version, datapos)
317                 if not data_is_air(blocknum) and blocknum in colors:
318                     if blocknum in CONTENT_WATER:
319                         water[(x, z)] += 1
320                         # Add dummy stuff for drawing sea without seabed
321                         stuff[(chunkxpos + x, chunkzpos + z)] = (
322                             chunkypos + y, blocknum, water[(x, z)])
323                     else:
324                         pixellist.remove((x, z))
325                         # Memorize information on the type and height of
326                         # the block and for drawing the picture.
327                         stuff[(chunkxpos + x, chunkzpos + z)] = (
328                             chunkypos + y, blocknum, water[(x, z)])
329                         break
330                 elif not data_is_air(blocknum) and blocknum not in colors:
331                     print "strange block: %s/%s/%s x: %d y: %d z: %d \
332 block id: %x" % (xhex, zhex, yhex, x, y, z, blocknum)
333
334 # Go through all sectors.
335 for n in range(len(xlist)):
336     #if n > 500:
337     #   break
338     if n % 200 == 0:
339         nowtime = time.time()
340         dtime = nowtime - starttime
341         try:
342             n_per_second = 1.0 * n / dtime
343         except ZeroDivisionError:
344             n_per_second = 0
345         if n_per_second != 0:
346             seconds_per_n = 1.0 / n_per_second
347             time_guess = seconds_per_n * len(xlist)
348             remaining_s = time_guess - dtime
349             remaining_minutes = int(remaining_s / 60)
350             remaining_s -= remaining_minutes * 60
351             print("Processing sector " + str(n) + " of " + str(len(xlist))
352                     + " (" + str(round(100.0 * n / len(xlist), 1)) + "%)"
353                     + " (ETA: " + str(remaining_minutes) + "m "
354                     + str(int(remaining_s)) + "s)")
355
356     xpos = xlist[n]
357     zpos = zlist[n]
358
359     xhex = int_to_hex3(xpos)
360     zhex = int_to_hex3(zpos)
361     xhex4 = int_to_hex4(xpos)
362     zhex4 = int_to_hex4(zpos)
363
364     sector1 = xhex4.lower() + zhex4.lower()
365     sector2 = xhex.lower() + "/" + zhex.lower()
366
367     ylist = []
368
369     sectortype = ""
370
371     if cur:
372         ps = getBlockAsInteger((xpos, 0, zpos))
373         cur.execute("SELECT `pos` FROM `blocks` WHERE `pos`>=? AND `pos`<?", (ps, ps + 4096))
374         while True:
375             r = cur.fetchone()
376             if not r:
377                 break
378             pos = getIntegerAsBlock(r[0])[1]
379             ylist.append(pos)
380             sectortype = "sqlite"
381     try:
382         for filename in os.listdir(path + "sectors/" + sector1):
383             if(filename != "meta"):
384                 pos = int(filename, 16)
385                 if(pos > 32767):
386                     pos -= 65536
387                 ylist.append(pos)
388                 sectortype = "old"
389     except OSError:
390         pass
391
392     if sectortype == "":
393         try:
394             for filename in os.listdir(path + "sectors2/" + sector2):
395                 if(filename != "meta"):
396                     pos = int(filename, 16)
397                     if(pos > 32767):
398                         pos -= 65536
399                     ylist.append(pos)
400                     sectortype = "new"
401         except OSError:
402             pass
403
404     if sectortype == "":
405         continue
406
407         #ylist.sort()
408         ylist = sorted(set(ylist))
409
410     # Make a list of pixels of the sector that are to be looked for.
411     pixellist = []
412     water = {}
413     for x in range(16):
414         for z in range(16):
415             pixellist.append((x, z))
416             water[(x, z)] = 0
417
418     # Go through the Y axis from top to bottom.
419     ylist2 = []
420     for ypos in reversed(ylist):
421
422         yhex = int_to_hex4(ypos)
423
424         filename = ""
425         if sectortype == "sqlite":
426             ps = getBlockAsInteger((xpos, ypos, zpos))
427             cur.execute("SELECT `data` FROM `blocks` WHERE `pos`==? LIMIT 1", (ps,))
428             r = cur.fetchone()
429             if not r:
430                 continue
431             f = Bytestream(r[0])
432         else:
433             if sectortype == "old":
434                 filename = path + "sectors/" + sector1 + "/" + yhex.lower()
435             else:
436                 filename = path + "sectors2/" + sector2 + "/" + yhex.lower()
437
438             f = file(filename, "rb")
439
440         # Let's just memorize these even though it's not really necessary.
441         version = ord(f.read(1))
442         flags = f.read(1)
443
444         # Checking day and night differs -flag
445         if not ord(flags) & 2:
446             ylist2.append((ypos, filename))
447             f.close()
448             continue
449
450         read_mapdata(f, version, pixellist, water)
451
452         # After finding all the pixels in the sector, we can move on to
453         # the next sector without having to continue the Y axis.
454         if(len(pixellist) == 0):
455             break
456
457     if len(pixellist) > 0:
458         for (ypos, filename) in ylist2:
459             ps = getBlockAsInteger((xpos, ypos, zpos))
460             cur.execute("SELECT `data` FROM `blocks` WHERE `pos`==? LIMIT 1", (ps,))
461             r = cur.fetchone()
462             if not r:
463                 continue
464             f = Bytestream(r[0])
465
466             version = ord(f.read(1))
467             flags = f.read(1)
468
469             read_mapdata(f, version, pixellist, water)
470
471             # After finding all the pixels in the sector, we can move on
472             # to the next sector without having to continue the Y axis.
473             if(len(pixellist) == 0):
474                 break
475
476 print "Drawing image"
477 # Drawing the picture
478 starttime = time.time()
479 n = 0
480 for (x, z) in stuff.iterkeys():
481     if n % 500000 == 0:
482         nowtime = time.time()
483         dtime = nowtime - starttime
484         try:
485             n_per_second = 1.0 * n / dtime
486         except ZeroDivisionError:
487             n_per_second = 0
488         if n_per_second != 0:
489             listlen = len(stuff)
490             seconds_per_n = 1.0 / n_per_second
491             time_guess = seconds_per_n * listlen
492             remaining_s = time_guess - dtime
493             remaining_minutes = int(remaining_s / 60)
494             remaining_s -= remaining_minutes * 60
495             print("Drawing pixel " + str(n) + " of " + str(listlen)
496                     + " (" + str(round(100.0 * n / listlen, 1)) + "%)"
497                     + " (ETA: " + str(remaining_minutes) + "m "
498                     + str(int(remaining_s)) + "s)")
499     n += 1
500
501     (r, g, b) = colors[stuff[(x, z)][1]]
502     # Comparing heights of a couple of adjacent blocks and changing
503     # brightness accordingly.
504     try:
505         c1 = stuff[(x - 1, z)][1]
506         c2 = stuff[(x, z + 1)][1]
507         c = stuff[(x, z)][1]
508         if c1 not in CONTENT_WATER and c2 not in CONTENT_WATER and \
509             c not in CONTENT_WATER:
510             y1 = stuff[(x - 1, z)][0]
511             y2 = stuff[(x, z + 1)][0]
512             y = stuff[(x, z)][0]
513
514             d = ((y - y1) + (y - y2)) * 12
515         else:
516             d = 0
517
518         if(d > 36):
519             d = 36
520
521         r = limit(r + d, 0, 255)
522         g = limit(g + d, 0, 255)
523         b = limit(b + d, 0, 255)
524     except:
525         pass
526
527     # Water
528     if(stuff[(x, z)][2] > 0):
529         r = int(r * .15 + colors[2][0] * .85)
530         g = int(g * .15 + colors[2][1] * .85)
531         b = int(b * .15 + colors[2][2] * .85)
532
533     impix[x - minx * 16 + border, h - 1 - (z - minz * 16) + border] = (r, g, b)
534
535
536 if draworigin:
537     draw.ellipse((minx * -16 - 5 + border, h - minz * -16 - 6 + border,
538         minx * -16 + 5 + border, h - minz * -16 + 4 + border),
539         outline=origincolor)
540
541 font = ImageFont.load_default()
542
543 if drawscale:
544     draw.text((24, 0), "X", font=font, fill=scalecolor)
545     draw.text((2, 24), "Z", font=font, fill=scalecolor)
546
547     for n in range(int(minx / -4) * -4, maxx, 4):
548         draw.text((minx * -16 + n * 16 + 2 + border, 0), str(n * 16),
549             font=font, fill=scalecolor)
550         draw.line((minx * -16 + n * 16 + border, 0,
551             minx * -16 + n * 16 + border, border - 1), fill=scalecolor)
552
553     for n in range(int(maxz / 4) * 4, minz, -4):
554         draw.text((2, h - 1 - (n * 16 - minz * 16) + border), str(n * 16),
555             font=font, fill=scalecolor)
556         draw.line((0, h - 1 - (n * 16 - minz * 16) + border, border - 1,
557             h - 1 - (n * 16 - minz * 16) + border), fill=scalecolor)
558
559 if drawplayers:
560     try:
561         for filename in os.listdir(path + "players"):
562             f = file(path + "players/" + filename)
563             lines = f.readlines()
564             name = ""
565             position = []
566             for line in lines:
567                 p = string.split(line)
568                 if p[0] == "name":
569                     name = p[2]
570                     print filename + ": name = " + name
571                 if p[0] == "position":
572                     position = string.split(p[2][1:-1], ",")
573                     print filename + ": position = " + p[2]
574             if len(name) > 0 and len(position) == 3:
575                 x = (int(float(position[0]) / 10 - minx * 16))
576                 z = int(h - (float(position[2]) / 10 - minz * 16))
577                 draw.ellipse((x - 2 + border, z - 2 + border,
578                     x + 2 + border, z + 2 + border), outline=playercolor)
579                 draw.text((x + 2 + border, z + 2 + border), name,
580                     font=font, fill=playercolor)
581             f.close()
582     except OSError:
583         pass
584
585 print "Saving"
586 im.save(output)