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