2 # -*- coding: utf-8 -*-
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.
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.
19 # Requires Python Imaging Library: http://www.pythonware.com/products/pil/
21 # Some speed-up: ...lol, actually it slows it down.
22 #import psyco ; psyco.full()
23 #from psyco.classes import *
32 from PIL import Image, ImageDraw, ImageFont, ImageColor
34 CONTENT_WATER = [2, 9]
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
76 return "%03X" % (i + 4096)
83 return "%04X" % (i + 65536)
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"
119 sector_xmin = -1500 / 16
120 sector_xmax = 1500 / 16
121 sector_zmin = -1500 / 16
122 sector_zmax = 1500 / 16
125 if o in ("-h", "--help"):
128 elif o in ("-i", "--input"):
130 elif o in ("-o", "--output"):
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":
143 elif o == "--drawplayers":
145 elif o == "--draworigin":
148 assert False, "unhandled option"
150 if path[-1:] != "/" and path[-1:] != "\\":
153 # Load color information for the blocks.
156 f = file("colors.txt")
158 f = file(os.path.join(os.path.dirname(__file__), "colors.txt"))
160 values = string.split(line)
161 colors[int(values[0], 16)] = (
170 # List all sectors to memory and calculate the width and heigth of the
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:
179 if z < sector_zmin or z > sector_zmax:
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:
190 if z < sector_zmin or z > sector_zmax:
200 w = (maxx - minx) * 16 + 16
201 h = (maxz - minz) * 16 + 16
203 print "w=" + str(w) + " h=" + str(h)
205 im = Image.new("RGB", (w + border, h + border), bgcolor)
206 draw = ImageDraw.Draw(im)
211 starttime = time.time()
215 return d in [126, 127, 254]
218 def read_blocknum(mapdata, version, datapos):
220 if mapdata[datapos] < 0x80:
221 return mapdata[datapos]
223 return (mapdata[datapos] << 4) | (mapdata[datapos + 0x2000] >> 4)
224 elif 16 <= version < 20:
225 return TRANSLATION_TABLE.get(mapdata[datapos], mapdata[datapos])
227 raise Exception("Unsupported map format: " + str(version))
230 def read_mapdata(f, version, pixellist, water):
231 global stuff # oh my :-)
233 dec_o = zlib.decompressobj()
235 mapdata = array.array("B", dec_o.decompress(f.read()))
241 if(len(mapdata) < 4096):
242 print "bad: " + xhex + "/" + zhex + "/" + yhex + " " + \
245 chunkxpos = xpos * 16
246 chunkypos = ypos * 16
247 chunkzpos = zpos * 16
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:
257 # Add dummy stuff for drawing sea without seabed
258 stuff[(chunkxpos + x, chunkzpos + z)] = (
259 chunkypos + y, blocknum, water[(x, z)])
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)])
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)
271 # Go through all sectors.
272 for n in range(len(xlist)):
276 nowtime = time.time()
277 dtime = nowtime - starttime
279 n_per_second = 1.0 * n / dtime
280 except ZeroDivisionError:
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)")
296 xhex = int_to_hex3(xpos)
297 zhex = int_to_hex3(zpos)
298 xhex4 = int_to_hex4(xpos)
299 zhex4 = int_to_hex4(zpos)
301 sector1 = xhex4.lower() + zhex4.lower()
302 sector2 = xhex.lower() + "/" + zhex.lower()
309 for filename in os.listdir(path + "sectors/" + sector1):
310 if(filename != "meta"):
311 pos = int(filename, 16)
319 if sectortype != "old":
321 for filename in os.listdir(path + "sectors2/" + sector2):
322 if(filename != "meta"):
323 pos = int(filename, 16)
336 # Make a list of pixels of the sector that are to be looked for.
341 pixellist.append((x, z))
344 # Go through the Y axis from top to bottom.
346 for ypos in reversed(ylist):
348 yhex = int_to_hex4(ypos)
351 if sectortype == "old":
352 filename = path + "sectors/" + sector1 + "/" + yhex.lower()
354 filename = path + "sectors2/" + sector2 + "/" + yhex.lower()
356 f = file(filename, "rb")
358 # Let's just memorize these even though it's not really necessary.
359 version = ord(f.read(1))
362 # Checking day and night differs -flag
363 if not ord(flags) & 2:
364 ylist2.append((ypos, filename))
368 read_mapdata(f, version, pixellist, water)
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):
375 if len(pixellist) > 0:
376 for (ypos, filename) in ylist2:
377 f = file(filename, "rb")
379 version = ord(f.read(1))
382 read_mapdata(f, version, pixellist, water)
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):
389 print "Drawing image"
390 # Drawing the picture
391 starttime = time.time()
393 for (x, z) in stuff.iterkeys():
395 nowtime = time.time()
396 dtime = nowtime - starttime
398 n_per_second = 1.0 * n / dtime
399 except ZeroDivisionError:
401 if n_per_second != 0:
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)")
414 (r, g, b) = colors[stuff[(x, z)][1]]
415 # Comparing heights of a couple of adjacent blocks and changing
416 # brightness accordingly.
418 c1 = stuff[(x - 1, z)][1]
419 c2 = stuff[(x, z + 1)][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]
427 d = ((y - y1) + (y - y2)) * 12
434 r = limit(r + d, 0, 255)
435 g = limit(g + d, 0, 255)
436 b = limit(b + d, 0, 255)
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)
446 impix[x - minx * 16 + border, h - 1 - (z - minz * 16) + border] = (r, g, b)
450 draw.ellipse((minx * -16 - 5 + border, h - minz * -16 - 6 + border,
451 minx * -16 + 5 + border, h - minz * -16 + 4 + border),
454 font = ImageFont.load_default()
457 draw.text((24, 0), "X", font=font, fill=scalecolor)
458 draw.text((2, 24), "Z", font=font, fill=scalecolor)
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)
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)
474 for filename in os.listdir(path + "players"):
475 f = file(path + "players/" + filename)
476 lines = f.readlines()
480 p = string.split(line)
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)