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