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