1#!/usr/bin/env python2 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 25import zlib 26import os 27import string 28import time 29import getopt 30import sys 31import array 32import cStringIO 33import traceback 34from PIL import Image, ImageDraw, ImageFont, ImageColor 35 36TRANSLATION_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 60def hex_to_int(h): 61 i = int(h, 16) 62 if(i > 2047): 63 i -= 4096 64 return i 65 66 67def hex4_to_int(h): 68 i = int(h, 16) 69 if(i > 32767): 70 i -= 65536 71 return i 72 73 74def int_to_hex3(i): 75 if(i < 0): 76 return "%03X" % (i + 4096) 77 else: 78 return "%03X" % i 79 80 81def int_to_hex4(i): 82 if(i < 0): 83 return "%04X" % (i + 65536) 84 else: 85 return "%04X" % i 86 87 88def getBlockAsInteger(p): 89 return p[2]*16777216 + p[1]*4096 + p[0] 90 91def unsignedToSigned(i, max_positive): 92 if i < max_positive: 93 return i 94 else: 95 return i - 2*max_positive 96 97def 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 105def limit(i, l, h): 106 if(i > h): 107 i = h 108 if(i < l): 109 i = l 110 return i 111 112def readU8(f): 113 return ord(f.read(1)) 114 115def readU16(f): 116 return ord(f.read(1))*256 + ord(f.read(1)) 117 118def readU32(f): 119 return ord(f.read(1))*256*256*256 + ord(f.read(1))*256*256 + ord(f.read(1))*256 + ord(f.read(1)) 120 121def readS32(f): 122 return unsignedToSigned(ord(f.read(1))*256*256*256 + ord(f.read(1))*256*256 + ord(f.read(1))*256 + ord(f.read(1)), 2**31) 123 124usagetext = """minetestmapper.py [options] 125 -i/--input <world_path> 126 -o/--output <output_image.png> 127 --bgcolor <color> 128 --scalecolor <color> 129 --playercolor <color> 130 --origincolor <color> 131 --drawscale 132 --drawplayers 133 --draworigin 134 --drawunderground 135Color format: '#000000'""" 136 137def usage(): 138 print(usagetext) 139 140try: 141 opts, args = getopt.getopt(sys.argv[1:], "hi:o:", ["help", "input=", 142 "output=", "bgcolor=", "scalecolor=", "origincolor=", 143 "playercolor=", "draworigin", "drawplayers", "drawscale", 144 "drawunderground"]) 145except getopt.GetoptError as err: 146 # print help information and exit: 147 print(str(err)) # will print something like "option -a not recognized" 148 usage() 149 sys.exit(2) 150 151path = None 152output = "map.png" 153border = 0 154scalecolor = "black" 155bgcolor = "white" 156origincolor = "red" 157playercolor = "red" 158drawscale = False 159drawplayers = False 160draworigin = False 161drawunderground = False 162 163sector_xmin = -1500 / 16 164sector_xmax = 1500 / 16 165sector_zmin = -1500 / 16 166sector_zmax = 1500 / 16 167 168for o, a in opts: 169 if o in ("-h", "--help"): 170 usage() 171 sys.exit() 172 elif o in ("-i", "--input"): 173 path = a 174 elif o in ("-o", "--output"): 175 output = a 176 elif o == "--bgcolor": 177 bgcolor = ImageColor.getrgb(a) 178 elif o == "--scalecolor": 179 scalecolor = ImageColor.getrgb(a) 180 elif o == "--playercolor": 181 playercolor = ImageColor.getrgb(a) 182 elif o == "--origincolor": 183 origincolor = ImageColor.getrgb(a) 184 elif o == "--drawscale": 185 drawscale = True 186 border = 40 187 elif o == "--drawplayers": 188 drawplayers = True 189 elif o == "--draworigin": 190 draworigin = True 191 elif o == "--drawunderground": 192 drawunderground = True 193 else: 194 assert False, "unhandled option" 195 196if path is None: 197 print("Please select world path (eg. -i ../worlds/yourworld) (or use --help)") 198 sys.exit(1) 199 200if path[-1:] != "/" and path[-1:] != "\\": 201 path = path + "/" 202 203# Load color information for the blocks. 204colors = {} 205try: 206 f = file("colors.txt") 207except IOError: 208 f = file(os.path.join(os.path.dirname(__file__), "colors.txt")) 209for line in f: 210 values = string.split(line) 211 if len(values) < 4: 212 continue 213 identifier = values[0] 214 is_hex = True 215 for c in identifier: 216 if c not in "0123456789abcdefABCDEF": 217 is_hex = False 218 break 219 if is_hex: 220 colors[int(values[0], 16)] = ( 221 int(values[1]), 222 int(values[2]), 223 int(values[3])) 224 else: 225 colors[values[0]] = ( 226 int(values[1]), 227 int(values[2]), 228 int(values[3])) 229f.close() 230 231#print("colors: "+repr(colors)) 232#sys.exit(1) 233 234xlist = [] 235zlist = [] 236 237# List all sectors to memory and calculate the width and heigth of the 238# resulting picture. 239 240conn = None 241cur = None 242if os.path.exists(path + "map.sqlite"): 243 import sqlite3 244 conn = sqlite3.connect(path + "map.sqlite") 245 cur = conn.cursor() 246 247 cur.execute("SELECT `pos` FROM `blocks`") 248 while True: 249 r = cur.fetchone() 250 if not r: 251 break 252 253 x, y, z = getIntegerAsBlock(r[0]) 254 255 if x < sector_xmin or x > sector_xmax: 256 continue 257 if z < sector_zmin or z > sector_zmax: 258 continue 259 260 xlist.append(x) 261 zlist.append(z) 262 263if os.path.exists(path + "sectors2"): 264 for filename in os.listdir(path + "sectors2"): 265 for filename2 in os.listdir(path + "sectors2/" + filename): 266 x = hex_to_int(filename) 267 z = hex_to_int(filename2) 268 if x < sector_xmin or x > sector_xmax: 269 continue 270 if z < sector_zmin or z > sector_zmax: 271 continue 272 xlist.append(x) 273 zlist.append(z) 274 275if os.path.exists(path + "sectors"): 276 for filename in os.listdir(path + "sectors"): 277 x = hex4_to_int(filename[:4]) 278 z = hex4_to_int(filename[-4:]) 279 if x < sector_xmin or x > sector_xmax: 280 continue 281 if z < sector_zmin or z > sector_zmax: 282 continue 283 xlist.append(x) 284 zlist.append(z) 285 286if len(xlist) == 0 or len(zlist) == 0: 287 print("World does not exist.") 288 sys.exit(1) 289 290# Get rid of doubles 291xlist, zlist = zip(*sorted(set(zip(xlist, zlist)))) 292 293minx = min(xlist) 294minz = min(zlist) 295maxx = max(xlist) 296maxz = max(zlist) 297 298w = (maxx - minx) * 16 + 16 299h = (maxz - minz) * 16 + 16 300 301print("Result image (w=" + str(w) + " h=" + str(h) + ") will be written to " 302 + output) 303 304im = Image.new("RGB", (w + border, h + border), bgcolor) 305draw = ImageDraw.Draw(im) 306impix = im.load() 307 308stuff = {} 309 310unknown_node_names = [] 311unknown_node_ids = [] 312 313starttime = time.time() 314 315CONTENT_WATER = 2 316 317def content_is_ignore(d): 318 return d in [0, "ignore"] 319 320def content_is_water(d): 321 return d in [2, 9] 322 323def content_is_air(d): 324 return d in [126, 127, 254, "air"] 325 326def read_content(mapdata, version, datapos): 327 if version >= 24: 328 return (mapdata[datapos*2] << 8) | (mapdata[datapos*2 + 1]) 329 elif version >= 20: 330 if mapdata[datapos] < 0x80: 331 return mapdata[datapos] 332 else: 333 return (mapdata[datapos] << 4) | (mapdata[datapos + 0x2000] >> 4) 334 elif 16 <= version < 20: 335 return TRANSLATION_TABLE.get(mapdata[datapos], mapdata[datapos]) 336 else: 337 raise Exception("Unsupported map format: " + str(version)) 338 339 340def read_mapdata(mapdata, version, pixellist, water, day_night_differs, id_to_name): 341 global stuff # oh my :-) 342 global unknown_node_names 343 global unknown_node_ids 344 345 if(len(mapdata) < 4096): 346 print("bad: " + xhex + "/" + zhex + "/" + yhex + " " + \ 347 str(len(mapdata))) 348 else: 349 chunkxpos = xpos * 16 350 chunkypos = ypos * 16 351 chunkzpos = zpos * 16 352 content = 0 353 datapos = 0 354 for (x, z) in reversed(pixellist): 355 for y in reversed(range(16)): 356 datapos = x + y * 16 + z * 256 357 content = read_content(mapdata, version, datapos) 358 # Try to convert id to name 359 try: 360 content = id_to_name[content] 361 except KeyError: 362 pass 363 364 if content_is_ignore(content): 365 pass 366 elif content_is_air(content): 367 pass 368 elif content_is_water(content): 369 water[(x, z)] += 1 370 # Add dummy stuff for drawing sea without seabed 371 stuff[(chunkxpos + x, chunkzpos + z)] = ( 372 chunkypos + y, content, water[(x, z)], day_night_differs) 373 elif content in colors: 374 # Memorize information on the type and height of 375 # the block and for drawing the picture. 376 stuff[(chunkxpos + x, chunkzpos + z)] = ( 377 chunkypos + y, content, water[(x, z)], day_night_differs) 378 pixellist.remove((x, z)) 379 break 380 else: 381 if type(content) == str: 382 if content not in unknown_node_names: 383 unknown_node_names.append(content) 384 #print("unknown node: %s/%s/%s x: %d y: %d z: %d block name: %s" 385 # % (xhex, zhex, yhex, x, y, z, content)) 386 else: 387 if content not in unknown_node_ids: 388 unknown_node_ids.append(content) 389 #print("unknown node: %s/%s/%s x: %d y: %d z: %d block id: %x" 390 # % (xhex, zhex, yhex, x, y, z, content)) 391 392 393# Go through all sectors. 394for n in range(len(xlist)): 395 #if n > 500: 396 # break 397 if n % 200 == 0: 398 nowtime = time.time() 399 dtime = nowtime - starttime 400 try: 401 n_per_second = 1.0 * n / dtime 402 except ZeroDivisionError: 403 n_per_second = 0 404 if n_per_second != 0: 405 seconds_per_n = 1.0 / n_per_second 406 time_guess = seconds_per_n * len(xlist) 407 remaining_s = time_guess - dtime 408 remaining_minutes = int(remaining_s / 60) 409 remaining_s -= remaining_minutes * 60 410 print("Processing sector " + str(n) + " of " + str(len(xlist)) 411 + " (" + str(round(100.0 * n / len(xlist), 1)) + "%)" 412 + " (ETA: " + str(remaining_minutes) + "m " 413 + str(int(remaining_s)) + "s)") 414 415 xpos = xlist[n] 416 zpos = zlist[n] 417 418 xhex = int_to_hex3(xpos) 419 zhex = int_to_hex3(zpos) 420 xhex4 = int_to_hex4(xpos) 421 zhex4 = int_to_hex4(zpos) 422 423 sector1 = xhex4.lower() + zhex4.lower() 424 sector2 = xhex.lower() + "/" + zhex.lower() 425 426 ylist = [] 427 428 sectortype = "" 429 430 if cur: 431 psmin = getBlockAsInteger((xpos, -2048, zpos)) 432 psmax = getBlockAsInteger((xpos, 2047, zpos)) 433 cur.execute("SELECT `pos` FROM `blocks` WHERE `pos`>=? AND `pos`<=? AND (`pos` - ?) % 4096 = 0", (psmin, psmax, psmin)) 434 while True: 435 r = cur.fetchone() 436 if not r: 437 break 438 pos = getIntegerAsBlock(r[0])[1] 439 ylist.append(pos) 440 sectortype = "sqlite" 441 try: 442 for filename in os.listdir(path + "sectors/" + sector1): 443 if(filename != "meta"): 444 pos = int(filename, 16) 445 if(pos > 32767): 446 pos -= 65536 447 ylist.append(pos) 448 sectortype = "old" 449 except OSError: 450 pass 451 452 if sectortype == "": 453 try: 454 for filename in os.listdir(path + "sectors2/" + sector2): 455 if(filename != "meta"): 456 pos = int(filename, 16) 457 if(pos > 32767): 458 pos -= 65536 459 ylist.append(pos) 460 sectortype = "new" 461 except OSError: 462 pass 463 464 if sectortype == "": 465 continue 466 467 ylist.sort() 468 469 # Make a list of pixels of the sector that are to be looked for. 470 pixellist = [] 471 water = {} 472 for x in range(16): 473 for z in range(16): 474 pixellist.append((x, z)) 475 water[(x, z)] = 0 476 477 # Go through the Y axis from top to bottom. 478 for ypos in reversed(ylist): 479 try: 480 #print("("+str(xpos)+","+str(ypos)+","+str(zpos)+")") 481 482 yhex = int_to_hex4(ypos) 483 484 if sectortype == "sqlite": 485 ps = getBlockAsInteger((xpos, ypos, zpos)) 486 cur.execute("SELECT `data` FROM `blocks` WHERE `pos`==? LIMIT 1", (ps,)) 487 r = cur.fetchone() 488 if not r: 489 continue 490 f = cStringIO.StringIO(r[0]) 491 else: 492 if sectortype == "old": 493 filename = path + "sectors/" + sector1 + "/" + yhex.lower() 494 else: 495 filename = path + "sectors2/" + sector2 + "/" + yhex.lower() 496 f = file(filename, "rb") 497 498 # Let's just memorize these even though it's not really necessary. 499 version = readU8(f) 500 flags = f.read(1) 501 502 #print("version="+str(version)) 503 #print("flags="+str(version)) 504 505 # Check flags 506 is_underground = ((ord(flags) & 1) != 0) 507 day_night_differs = ((ord(flags) & 2) != 0) 508 lighting_expired = ((ord(flags) & 4) != 0) 509 generated = ((ord(flags) & 8) != 0) 510 511 #print("is_underground="+str(is_underground)) 512 #print("day_night_differs="+str(day_night_differs)) 513 #print("lighting_expired="+str(lighting_expired)) 514 #print("generated="+str(generated)) 515 516 if version >= 22: 517 content_width = readU8(f) 518 params_width = readU8(f) 519 520 # Node data 521 dec_o = zlib.decompressobj() 522 try: 523 mapdata = array.array("B", dec_o.decompress(f.read())) 524 except: 525 mapdata = [] 526 527 # Reuse the unused tail of the file 528 f.close(); 529 f = cStringIO.StringIO(dec_o.unused_data) 530 #print("unused data: "+repr(dec_o.unused_data)) 531 532 # zlib-compressed node metadata list 533 dec_o = zlib.decompressobj() 534 try: 535 metaliststr = array.array("B", dec_o.decompress(f.read())) 536 # And do nothing with it 537 except: 538 metaliststr = [] 539 540 # Reuse the unused tail of the file 541 f.close(); 542 f = cStringIO.StringIO(dec_o.unused_data) 543 #print("* dec_o.unused_data: "+repr(dec_o.unused_data)) 544 data_after_node_metadata = dec_o.unused_data 545 546 if version <= 21: 547 # mapblockobject_count 548 readU16(f) 549 550 if version == 23: 551 readU8(f) # Unused node timer version (always 0) 552 if version == 24: 553 ver = readU8(f) 554 if ver == 1: 555 num = readU16(f) 556 for i in range(0,num): 557 readU16(f) 558 readS32(f) 559 readS32(f) 560 561 static_object_version = readU8(f) 562 static_object_count = readU16(f) 563 for i in range(0, static_object_count): 564 # u8 type (object type-id) 565 object_type = readU8(f) 566 # s32 pos_x_nodes * 10000 567 pos_x_nodes = readS32(f)/10000 568 # s32 pos_y_nodes * 10000 569 pos_y_nodes = readS32(f)/10000 570 # s32 pos_z_nodes * 10000 571 pos_z_nodes = readS32(f)/10000 572 # u16 data_size 573 data_size = readU16(f) 574 # u8[data_size] data 575 data = f.read(data_size) 576 577 timestamp = readU32(f) 578 #print("* timestamp="+str(timestamp)) 579 580 id_to_name = {} 581 if version >= 22: 582 name_id_mapping_version = readU8(f) 583 num_name_id_mappings = readU16(f) 584 #print("* num_name_id_mappings: "+str(num_name_id_mappings)) 585 for i in range(0, num_name_id_mappings): 586 node_id = readU16(f) 587 name_len = readU16(f) 588 name = f.read(name_len) 589 #print(str(node_id)+" = "+name) 590 id_to_name[node_id] = name 591 592 # Node timers 593 if version >= 25: 594 timer_size = readU8(f) 595 num = readU16(f) 596 for i in range(0,num): 597 readU16(f) 598 readS32(f) 599 readS32(f) 600 601 read_mapdata(mapdata, version, pixellist, water, day_night_differs, id_to_name) 602 603 # After finding all the pixels in the sector, we can move on to 604 # the next sector without having to continue the Y axis. 605 if(len(pixellist) == 0): 606 break 607 except Exception as e: 608 print("Error at ("+str(xpos)+","+str(ypos)+","+str(zpos)+"): "+str(e)) 609 sys.stdout.write("Block data: ") 610 for c in r[0]: 611 sys.stdout.write("%2.2x "%ord(c)) 612 sys.stdout.write(os.linesep) 613 sys.stdout.write("Data after node metadata: ") 614 for c in data_after_node_metadata: 615 sys.stdout.write("%2.2x "%ord(c)) 616 sys.stdout.write(os.linesep) 617 traceback.print_exc() 618 619print("Drawing image") 620# Drawing the picture 621starttime = time.time() 622n = 0 623for (x, z) in stuff.iterkeys(): 624 if n % 500000 == 0: 625 nowtime = time.time() 626 dtime = nowtime - starttime 627 try: 628 n_per_second = 1.0 * n / dtime 629 except ZeroDivisionError: 630 n_per_second = 0 631 if n_per_second != 0: 632 listlen = len(stuff) 633 seconds_per_n = 1.0 / n_per_second 634 time_guess = seconds_per_n * listlen 635 remaining_s = time_guess - dtime 636 remaining_minutes = int(remaining_s / 60) 637 remaining_s -= remaining_minutes * 60 638 print("Drawing pixel " + str(n) + " of " + str(listlen) 639 + " (" + str(round(100.0 * n / listlen, 1)) + "%)" 640 + " (ETA: " + str(remaining_minutes) + "m " 641 + str(int(remaining_s)) + "s)") 642 n += 1 643 644 (r, g, b) = colors[stuff[(x, z)][1]] 645 646 dnd = stuff[(x, z)][3] # day/night differs? 647 if not dnd and not drawunderground: 648 if stuff[(x, z)][2] > 0: # water 649 (r, g, b) = colors[CONTENT_WATER] 650 else: 651 continue 652 653 # Comparing heights of a couple of adjacent blocks and changing 654 # brightness accordingly. 655 try: 656 c = stuff[(x, z)][1] 657 c1 = stuff[(x - 1, z)][1] 658 c2 = stuff[(x, z + 1)][1] 659 dnd1 = stuff[(x - 1, z)][3] 660 dnd2 = stuff[(x, z + 1)][3] 661 if not dnd: 662 d = -69 663 elif not content_is_water(c1) and not content_is_water(c2) and \ 664 not content_is_water(c): 665 y = stuff[(x, z)][0] 666 y1 = stuff[(x - 1, z)][0] if dnd1 else y 667 y2 = stuff[(x, z + 1)][0] if dnd2 else y 668 d = ((y - y1) + (y - y2)) * 12 669 else: 670 d = 0 671 672 if(d > 36): 673 d = 36 674 675 r = limit(r + d, 0, 255) 676 g = limit(g + d, 0, 255) 677 b = limit(b + d, 0, 255) 678 except: 679 pass 680 681 # Water 682 if(stuff[(x, z)][2] > 0): 683 r = int(r * .15 + colors[2][0] * .85) 684 g = int(g * .15 + colors[2][1] * .85) 685 b = int(b * .15 + colors[2][2] * .85) 686 687 impix[x - minx * 16 + border, h - 1 - (z - minz * 16) + border] = (r, g, b) 688 689 690if draworigin: 691 draw.ellipse((minx * -16 - 5 + border, h - minz * -16 - 6 + border, 692 minx * -16 + 5 + border, h - minz * -16 + 4 + border), 693 outline=origincolor) 694 695font = ImageFont.load_default() 696 697if drawscale: 698 draw.text((24, 0), "X", font=font, fill=scalecolor) 699 draw.text((2, 24), "Z", font=font, fill=scalecolor) 700 701 for n in range(int(minx / -4) * -4, maxx, 4): 702 draw.text((minx * -16 + n * 16 + 2 + border, 0), str(n * 16), 703 font=font, fill=scalecolor) 704 draw.line((minx * -16 + n * 16 + border, 0, 705 minx * -16 + n * 16 + border, border - 1), fill=scalecolor) 706 707 for n in range(int(maxz / 4) * 4, minz, -4): 708 draw.text((2, h - 1 - (n * 16 - minz * 16) + border), str(n * 16), 709 font=font, fill=scalecolor) 710 draw.line((0, h - 1 - (n * 16 - minz * 16) + border, border - 1, 711 h - 1 - (n * 16 - minz * 16) + border), fill=scalecolor) 712 713if drawplayers: 714 try: 715 for filename in os.listdir(path + "players"): 716 f = file(path + "players/" + filename) 717 lines = f.readlines() 718 name = "" 719 position = [] 720 for line in lines: 721 p = string.split(line) 722 if p[0] == "name": 723 name = p[2] 724 print(filename + ": name = " + name) 725 if p[0] == "position": 726 position = string.split(p[2][1:-1], ",") 727 print(filename + ": position = " + p[2]) 728 if len(name) > 0 and len(position) == 3: 729 x = (int(float(position[0]) / 10 - minx * 16)) 730 z = int(h - (float(position[2]) / 10 - minz * 16)) 731 draw.ellipse((x - 2 + border, z - 2 + border, 732 x + 2 + border, z + 2 + border), outline=playercolor) 733 draw.text((x + 2 + border, z + 2 + border), name, 734 font=font, fill=playercolor) 735 f.close() 736 except OSError: 737 pass 738 739print("Saving") 740im.save(output) 741 742if unknown_node_names: 743 sys.stdout.write("Unknown node names:") 744 for name in unknown_node_names: 745 sys.stdout.write(" "+name) 746 sys.stdout.write(os.linesep) 747if unknown_node_ids: 748 sys.stdout.write("Unknown node ids:") 749 for node_id in unknown_node_ids: 750 sys.stdout.write(" "+str(hex(node_id))) 751 sys.stdout.write(os.linesep) 752 753