minetestmapper.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. # This program is free software. It comes without any warranty, to
  4. # the extent permitted by applicable law. You can redistribute it
  5. # and/or modify it under the terms of the Do What The Fuck You Want
  6. # To Public License, Version 2, as published by Sam Hocevar. See
  7. # COPYING for more details.
  8. # Made by Jogge, modified by celeron55
  9. # 2011-05-29: j0gge: initial release
  10. # 2011-05-30: celeron55: simultaneous support for sectors/sectors2, removed
  11. # 2011-06-02: j0gge: command line parameters, coordinates, players, ...
  12. # 2011-06-04: celeron55: added #!/usr/bin/python2 and converted \r\n to \n
  13. # to make it easily executable on Linux
  14. # 2011-07-30: WF: Support for content types extension, refactoring
  15. # 2011-07-30: erlehmann: PEP 8 compliance.
  16. # Requires Python Imaging Library: http://www.pythonware.com/products/pil/
  17. # Some speed-up: ...lol, actually it slows it down.
  18. #import psyco ; psyco.full()
  19. #from psyco.classes import *
  20. import zlib
  21. import os
  22. import string
  23. import time
  24. import getopt
  25. import sys
  26. import array
  27. import cStringIO
  28. from PIL import Image, ImageDraw, ImageFont, ImageColor
  29. TRANSLATION_TABLE = {
  30. 1: 0x800, # CONTENT_GRASS
  31. 4: 0x801, # CONTENT_TREE
  32. 5: 0x802, # CONTENT_LEAVES
  33. 6: 0x803, # CONTENT_GRASS_FOOTSTEPS
  34. 7: 0x804, # CONTENT_MESE
  35. 8: 0x805, # CONTENT_MUD
  36. 10: 0x806, # CONTENT_CLOUD
  37. 11: 0x807, # CONTENT_COALSTONE
  38. 12: 0x808, # CONTENT_WOOD
  39. 13: 0x809, # CONTENT_SAND
  40. 18: 0x80a, # CONTENT_COBBLE
  41. 19: 0x80b, # CONTENT_STEEL
  42. 20: 0x80c, # CONTENT_GLASS
  43. 22: 0x80d, # CONTENT_MOSSYCOBBLE
  44. 23: 0x80e, # CONTENT_GRAVEL
  45. 24: 0x80f, # CONTENT_SANDSTONE
  46. 25: 0x810, # CONTENT_CACTUS
  47. 26: 0x811, # CONTENT_BRICK
  48. 27: 0x812, # CONTENT_CLAY
  49. 28: 0x813, # CONTENT_PAPYRUS
  50. 29: 0x814} # CONTENT_BOOKSHELF
  51. def hex_to_int(h):
  52. i = int(h, 16)
  53. if(i > 2047):
  54. i -= 4096
  55. return i
  56. def hex4_to_int(h):
  57. i = int(h, 16)
  58. if(i > 32767):
  59. i -= 65536
  60. return i
  61. def int_to_hex3(i):
  62. if(i < 0):
  63. return "%03X" % (i + 4096)
  64. else:
  65. return "%03X" % i
  66. def int_to_hex4(i):
  67. if(i < 0):
  68. return "%04X" % (i + 65536)
  69. else:
  70. return "%04X" % i
  71. def getBlockAsInteger(p):
  72. return p[2]*16777216 + p[1]*4096 + p[0]
  73. def unsignedToSigned(i, max_positive):
  74. if i < max_positive:
  75. return i
  76. else:
  77. return i - 2*max_positive
  78. def getIntegerAsBlock(i):
  79. x = unsignedToSigned(i % 4096, 2048)
  80. i = int((i - x) / 4096)
  81. y = unsignedToSigned(i % 4096, 2048)
  82. i = int((i - y) / 4096)
  83. z = unsignedToSigned(i % 4096, 2048)
  84. return x,y,z
  85. def limit(i, l, h):
  86. if(i > h):
  87. i = h
  88. if(i < l):
  89. i = l
  90. return i
  91. def usage():
  92. print("TODO: Help")
  93. try:
  94. opts, args = getopt.getopt(sys.argv[1:], "hi:o:", ["help", "input=",
  95. "output=", "bgcolor=", "scalecolor=", "origincolor=",
  96. "playercolor=", "draworigin", "drawplayers", "drawscale",
  97. "drawunderground"])
  98. except getopt.GetoptError as err:
  99. # print help information and exit:
  100. print(str(err)) # will print something like "option -a not recognized"
  101. usage()
  102. sys.exit(2)
  103. path = "../world/"
  104. output = "map.png"
  105. border = 0
  106. scalecolor = "black"
  107. bgcolor = "white"
  108. origincolor = "red"
  109. playercolor = "red"
  110. drawscale = False
  111. drawplayers = False
  112. draworigin = False
  113. drawunderground = False
  114. sector_xmin = -1500 / 16
  115. sector_xmax = 1500 / 16
  116. sector_zmin = -1500 / 16
  117. sector_zmax = 1500 / 16
  118. for o, a in opts:
  119. if o in ("-h", "--help"):
  120. usage()
  121. sys.exit()
  122. elif o in ("-i", "--input"):
  123. path = a
  124. elif o in ("-o", "--output"):
  125. output = a
  126. elif o == "--bgcolor":
  127. bgcolor = ImageColor.getrgb(a)
  128. elif o == "--scalecolor":
  129. scalecolor = ImageColor.getrgb(a)
  130. elif o == "--playercolor":
  131. playercolor = ImageColor.getrgb(a)
  132. elif o == "--origincolor":
  133. origincolor = ImageColor.getrgb(a)
  134. elif o == "--drawscale":
  135. drawscale = True
  136. border = 40
  137. elif o == "--drawplayers":
  138. drawplayers = True
  139. elif o == "--draworigin":
  140. draworigin = True
  141. elif o == "--drawunderground":
  142. drawunderground = True
  143. else:
  144. assert False, "unhandled option"
  145. if path[-1:] != "/" and path[-1:] != "\\":
  146. path = path + "/"
  147. # Load color information for the blocks.
  148. colors = {}
  149. try:
  150. f = file("colors.txt")
  151. except IOError:
  152. f = file(os.path.join(os.path.dirname(__file__), "colors.txt"))
  153. for line in f:
  154. values = string.split(line)
  155. colors[int(values[0], 16)] = (
  156. int(values[1]),
  157. int(values[2]),
  158. int(values[3]))
  159. f.close()
  160. xlist = []
  161. zlist = []
  162. # List all sectors to memory and calculate the width and heigth of the
  163. # resulting picture.
  164. conn = None
  165. cur = None
  166. if os.path.exists(path + "map.sqlite"):
  167. import sqlite3
  168. conn = sqlite3.connect(path + "map.sqlite")
  169. cur = conn.cursor()
  170. cur.execute("SELECT `pos` FROM `blocks`")
  171. while True:
  172. r = cur.fetchone()
  173. if not r:
  174. break
  175. x, y, z = getIntegerAsBlock(r[0])
  176. if x < sector_xmin or x > sector_xmax:
  177. continue
  178. if z < sector_zmin or z > sector_zmax:
  179. continue
  180. xlist.append(x)
  181. zlist.append(z)
  182. if os.path.exists(path + "sectors2"):
  183. for filename in os.listdir(path + "sectors2"):
  184. for filename2 in os.listdir(path + "sectors2/" + filename):
  185. x = hex_to_int(filename)
  186. z = hex_to_int(filename2)
  187. if x < sector_xmin or x > sector_xmax:
  188. continue
  189. if z < sector_zmin or z > sector_zmax:
  190. continue
  191. xlist.append(x)
  192. zlist.append(z)
  193. if os.path.exists(path + "sectors"):
  194. for filename in os.listdir(path + "sectors"):
  195. x = hex4_to_int(filename[:4])
  196. z = hex4_to_int(filename[-4:])
  197. if x < sector_xmin or x > sector_xmax:
  198. continue
  199. if z < sector_zmin or z > sector_zmax:
  200. continue
  201. xlist.append(x)
  202. zlist.append(z)
  203. # Get rid of doubles
  204. xlist, zlist = zip(*sorted(set(zip(xlist, zlist))))
  205. minx = min(xlist)
  206. minz = min(zlist)
  207. maxx = max(xlist)
  208. maxz = max(zlist)
  209. w = (maxx - minx) * 16 + 16
  210. h = (maxz - minz) * 16 + 16
  211. print("w=" + str(w) + " h=" + str(h))
  212. im = Image.new("RGB", (w + border, h + border), bgcolor)
  213. draw = ImageDraw.Draw(im)
  214. impix = im.load()
  215. stuff = {}
  216. starttime = time.time()
  217. CONTENT_WATER = 2
  218. def content_is_water(d):
  219. return d in [2, 9]
  220. def content_is_air(d):
  221. return d in [126, 127, 254]
  222. def read_content(mapdata, version, datapos):
  223. if version == 20:
  224. if mapdata[datapos] < 0x80:
  225. return mapdata[datapos]
  226. else:
  227. return (mapdata[datapos] << 4) | (mapdata[datapos + 0x2000] >> 4)
  228. elif 16 <= version < 20:
  229. return TRANSLATION_TABLE.get(mapdata[datapos], mapdata[datapos])
  230. else:
  231. raise Exception("Unsupported map format: " + str(version))
  232. def read_mapdata(f, version, pixellist, water, day_night_differs):
  233. global stuff # oh my :-)
  234. dec_o = zlib.decompressobj()
  235. try:
  236. mapdata = array.array("B", dec_o.decompress(f.read()))
  237. except:
  238. mapdata = []
  239. f.close()
  240. if(len(mapdata) < 4096):
  241. print("bad: " + xhex + "/" + zhex + "/" + yhex + " " + \
  242. str(len(mapdata)))
  243. else:
  244. chunkxpos = xpos * 16
  245. chunkypos = ypos * 16
  246. chunkzpos = zpos * 16
  247. content = 0
  248. datapos = 0
  249. for (x, z) in reversed(pixellist):
  250. for y in reversed(range(16)):
  251. datapos = x + y * 16 + z * 256
  252. content = read_content(mapdata, version, datapos)
  253. if content_is_air(content):
  254. pass
  255. elif content_is_water(content):
  256. water[(x, z)] += 1
  257. # Add dummy stuff for drawing sea without seabed
  258. stuff[(chunkxpos + x, chunkzpos + z)] = (
  259. chunkypos + y, content, water[(x, z)], day_night_differs)
  260. elif content in colors:
  261. # Memorize information on the type and height of
  262. # the block and for drawing the picture.
  263. stuff[(chunkxpos + x, chunkzpos + z)] = (
  264. chunkypos + y, content, water[(x, z)], day_night_differs)
  265. pixellist.remove((x, z))
  266. break
  267. else:
  268. print("strange block: %s/%s/%s x: %d y: %d z: %d \
  269. block id: %x" % (xhex, zhex, yhex, x, y, z, content))
  270. # Go through all sectors.
  271. for n in range(len(xlist)):
  272. #if n > 500:
  273. # break
  274. if n % 200 == 0:
  275. nowtime = time.time()
  276. dtime = nowtime - starttime
  277. try:
  278. n_per_second = 1.0 * n / dtime
  279. except ZeroDivisionError:
  280. n_per_second = 0
  281. if n_per_second != 0:
  282. seconds_per_n = 1.0 / n_per_second
  283. time_guess = seconds_per_n * len(xlist)
  284. remaining_s = time_guess - dtime
  285. remaining_minutes = int(remaining_s / 60)
  286. remaining_s -= remaining_minutes * 60
  287. print("Processing sector " + str(n) + " of " + str(len(xlist))
  288. + " (" + str(round(100.0 * n / len(xlist), 1)) + "%)"
  289. + " (ETA: " + str(remaining_minutes) + "m "
  290. + str(int(remaining_s)) + "s)")
  291. xpos = xlist[n]
  292. zpos = zlist[n]
  293. xhex = int_to_hex3(xpos)
  294. zhex = int_to_hex3(zpos)
  295. xhex4 = int_to_hex4(xpos)
  296. zhex4 = int_to_hex4(zpos)
  297. sector1 = xhex4.lower() + zhex4.lower()
  298. sector2 = xhex.lower() + "/" + zhex.lower()
  299. ylist = []
  300. sectortype = ""
  301. if cur:
  302. psmin = getBlockAsInteger((xpos, -2048, zpos))
  303. psmax = getBlockAsInteger((xpos, 2047, zpos))
  304. cur.execute("SELECT `pos` FROM `blocks` WHERE `pos`>=? AND `pos`<=? AND (`pos` - ?) % 4096 = 0", (psmin, psmax, psmin))
  305. while True:
  306. r = cur.fetchone()
  307. if not r:
  308. break
  309. pos = getIntegerAsBlock(r[0])[1]
  310. ylist.append(pos)
  311. sectortype = "sqlite"
  312. try:
  313. for filename in os.listdir(path + "sectors/" + sector1):
  314. if(filename != "meta"):
  315. pos = int(filename, 16)
  316. if(pos > 32767):
  317. pos -= 65536
  318. ylist.append(pos)
  319. sectortype = "old"
  320. except OSError:
  321. pass
  322. if sectortype == "":
  323. try:
  324. for filename in os.listdir(path + "sectors2/" + sector2):
  325. if(filename != "meta"):
  326. pos = int(filename, 16)
  327. if(pos > 32767):
  328. pos -= 65536
  329. ylist.append(pos)
  330. sectortype = "new"
  331. except OSError:
  332. pass
  333. if sectortype == "":
  334. continue
  335. ylist.sort()
  336. # Make a list of pixels of the sector that are to be looked for.
  337. pixellist = []
  338. water = {}
  339. for x in range(16):
  340. for z in range(16):
  341. pixellist.append((x, z))
  342. water[(x, z)] = 0
  343. # Go through the Y axis from top to bottom.
  344. for ypos in reversed(ylist):
  345. yhex = int_to_hex4(ypos)
  346. if sectortype == "sqlite":
  347. ps = getBlockAsInteger((xpos, ypos, zpos))
  348. cur.execute("SELECT `data` FROM `blocks` WHERE `pos`==? LIMIT 1", (ps,))
  349. r = cur.fetchone()
  350. if not r:
  351. continue
  352. f = cStringIO.StringIO(r[0])
  353. else:
  354. if sectortype == "old":
  355. filename = path + "sectors/" + sector1 + "/" + yhex.lower()
  356. else:
  357. filename = path + "sectors2/" + sector2 + "/" + yhex.lower()
  358. f = file(filename, "rb")
  359. # Let's just memorize these even though it's not really necessary.
  360. version = ord(f.read(1))
  361. flags = f.read(1)
  362. # Checking day and night differs -flag
  363. day_night_differs = ((ord(flags) & 2) != 0)
  364. read_mapdata(f, version, pixellist, water, day_night_differs)
  365. # After finding all the pixels in the sector, we can move on to
  366. # the next sector without having to continue the Y axis.
  367. if(len(pixellist) == 0):
  368. break
  369. print("Drawing image")
  370. # Drawing the picture
  371. starttime = time.time()
  372. n = 0
  373. for (x, z) in stuff.iterkeys():
  374. if n % 500000 == 0:
  375. nowtime = time.time()
  376. dtime = nowtime - starttime
  377. try:
  378. n_per_second = 1.0 * n / dtime
  379. except ZeroDivisionError:
  380. n_per_second = 0
  381. if n_per_second != 0:
  382. listlen = len(stuff)
  383. seconds_per_n = 1.0 / n_per_second
  384. time_guess = seconds_per_n * listlen
  385. remaining_s = time_guess - dtime
  386. remaining_minutes = int(remaining_s / 60)
  387. remaining_s -= remaining_minutes * 60
  388. print("Drawing pixel " + str(n) + " of " + str(listlen)
  389. + " (" + str(round(100.0 * n / listlen, 1)) + "%)"
  390. + " (ETA: " + str(remaining_minutes) + "m "
  391. + str(int(remaining_s)) + "s)")
  392. n += 1
  393. (r, g, b) = colors[stuff[(x, z)][1]]
  394. dnd = stuff[(x, z)][3] # day/night differs?
  395. if not dnd and not drawunderground:
  396. if stuff[(x, z)][2] > 0: # water
  397. (r, g, b) = colors[CONTENT_WATER]
  398. else:
  399. continue
  400. # Comparing heights of a couple of adjacent blocks and changing
  401. # brightness accordingly.
  402. try:
  403. c = stuff[(x, z)][1]
  404. c1 = stuff[(x - 1, z)][1]
  405. c2 = stuff[(x, z + 1)][1]
  406. dnd1 = stuff[(x - 1, z)][3]
  407. dnd2 = stuff[(x, z + 1)][3]
  408. if not dnd:
  409. d = -69
  410. elif not content_is_water(c1) and not content_is_water(c2) and \
  411. not content_is_water(c):
  412. y = stuff[(x, z)][0]
  413. y1 = stuff[(x - 1, z)][0] if dnd1 else y
  414. y2 = stuff[(x, z + 1)][0] if dnd2 else y
  415. d = ((y - y1) + (y - y2)) * 12
  416. else:
  417. d = 0
  418. if(d > 36):
  419. d = 36
  420. r = limit(r + d, 0, 255)
  421. g = limit(g + d, 0, 255)
  422. b = limit(b + d, 0, 255)
  423. except:
  424. pass
  425. # Water
  426. if(stuff[(x, z)][2] > 0):
  427. r = int(r * .15 + colors[2][0] * .85)
  428. g = int(g * .15 + colors[2][1] * .85)
  429. b = int(b * .15 + colors[2][2] * .85)
  430. impix[x - minx * 16 + border, h - 1 - (z - minz * 16) + border] = (r, g, b)
  431. if draworigin:
  432. draw.ellipse((minx * -16 - 5 + border, h - minz * -16 - 6 + border,
  433. minx * -16 + 5 + border, h - minz * -16 + 4 + border),
  434. outline=origincolor)
  435. font = ImageFont.load_default()
  436. if drawscale:
  437. draw.text((24, 0), "X", font=font, fill=scalecolor)
  438. draw.text((2, 24), "Z", font=font, fill=scalecolor)
  439. for n in range(int(minx / -4) * -4, maxx, 4):
  440. draw.text((minx * -16 + n * 16 + 2 + border, 0), str(n * 16),
  441. font=font, fill=scalecolor)
  442. draw.line((minx * -16 + n * 16 + border, 0,
  443. minx * -16 + n * 16 + border, border - 1), fill=scalecolor)
  444. for n in range(int(maxz / 4) * 4, minz, -4):
  445. draw.text((2, h - 1 - (n * 16 - minz * 16) + border), str(n * 16),
  446. font=font, fill=scalecolor)
  447. draw.line((0, h - 1 - (n * 16 - minz * 16) + border, border - 1,
  448. h - 1 - (n * 16 - minz * 16) + border), fill=scalecolor)
  449. if drawplayers:
  450. try:
  451. for filename in os.listdir(path + "players"):
  452. f = file(path + "players/" + filename)
  453. lines = f.readlines()
  454. name = ""
  455. position = []
  456. for line in lines:
  457. p = string.split(line)
  458. if p[0] == "name":
  459. name = p[2]
  460. print(filename + ": name = " + name)
  461. if p[0] == "position":
  462. position = string.split(p[2][1:-1], ",")
  463. print(filename + ": position = " + p[2])
  464. if len(name) > 0 and len(position) == 3:
  465. x = (int(float(position[0]) / 10 - minx * 16))
  466. z = int(h - (float(position[2]) / 10 - minz * 16))
  467. draw.ellipse((x - 2 + border, z - 2 + border,
  468. x + 2 + border, z + 2 + border), outline=playercolor)
  469. draw.text((x + 2 + border, z + 2 + border), name,
  470. font=font, fill=playercolor)
  471. f.close()
  472. except OSError:
  473. pass
  474. print("Saving")
  475. im.save(output)