def decompress(f, offset, game, region, level, level_type): """Returns a decompressed map. f = file offset = Self explanatory. game = Name of game, used for the filename for logging. level = Name of level, used for the filename for logging. level_type = Type of level (e.g. Stilt), used for the filename for logging.""" width = rom_utilities.readbyte(f) height = rom_utilities.readbyte(f) print "Width: {}; Height: {}".format(width,height) offset += 2 read_map = True level_map = [] for i in range(height): level_map.append([]) x = 0 y = 0 buf = [] #Log to external file for debugging if settings.maplog == True: if not os.path.exists(os.path.join(os.getcwd(), game)): os.makedirs(os.path.join(os.getcwd(), game)) if not os.path.exists(os.path.join(os.getcwd(), game, 'Logs')): os.makedirs(os.path.join(os.getcwd(), game, 'Logs')) filename = "{}_{}_Map_{}_log.txt".format(game,region,level_info.level_names[game][level].replace(" ","_")) filename = os.path.join(os.getcwd(), game, 'Logs', filename) maplog = open(filename, "wb") print "Writing log of map decompression to {}.".format(filename) rom_utilities.write_log("Width: {}; Height: {}".format(width,height),maplog) else: maplog = None #Decompression starts here, read one nibble (4 bits) at a time nib = Nibble(f, maplog, offset, settings.maplog) while read_map == True: nibble = nib.readnibble() rom_utilities.write_log("Nibble read: {:#x}".format(nibble),maplog) #Nibbles 0xC, 0xD, 0xE, and 0xF are special cases, as are nibble pairs 0xBE and 0xBF #(Note: Figuring this out would have been IMPOSSIBLE without BGB's debugger!!) if nibble == 0xB: temp = nibble nibble = nib.readnibble() rom_utilities.write_log("Nibble read: {:#x}".format(nibble),maplog) if nibble < 0xE: #Normal byte, this means nibble pair is from 0xB0-0xBD -- this is uncompressed buf.append((temp << 4) + nibble) elif nibble == 0xE: #0xBE #Decompress certain tiles, increasing tile value by 1 each time (useful for long tile structures) tile = nib.readnibble(count=2) rom_utilities.write_log("Tile: {:#04x}".format(tile),maplog) length = nib.readnibble() rom_utilities.write_log("Length: {:#x} + 0x3 -> {:#x}".format(length,length+0x3),maplog) for i in range(length+3): buf.append(tile) tile += 1 elif nibble == 0xF: #0xBF #Meant for 32x16-tile structures tile1 = nib.readnibble(count=2) tile2 = nib.readnibble(count=2) length = nib.readnibble() rom_utilities.write_log("Tile 1: {:#04x}; Tile 2: {:#04x}; Length: {:#x} + 0x2 -> {:#x}".format(tile1, tile2, length, length+0x2),maplog) for i in range(length+2): buf.append(tile1) buf.append(tile2) elif nibble == 0xC: #Repeat previously used tile sequence, allows the same structure over and over rew = nib.readnibble(count=2) if rew % 2 == 1: #Odd number rew = rew / 2 + 1 rew = rew + (nib.readnibble() << 7) else: #Even number rew = rew / 2 + 1 rom_utilities.write_log("Rewind: {:#04x}".format(rew),maplog) length = nib.readnibble() if length == 0xF: length = nib.readnibble(count=2) rom_utilities.write_log("Length: {:#04x} + 0x04 -> {:#04x}".format(length, length+0x4),maplog) for i in range(length+4): ptr = y*width+x - rew ptr_y = ptr / width ptr_x = ptr % width tile = level_map[ptr_y][ptr_x] rom_utilities.write_log("Pointer: {}; x: {}; y: {}; tile: {:#04x}; RAM: {:#x} -> {:#x}".format(ptr, ptr_x, ptr_y, tile, 0xC600+(height+4)*2+ptr, 0xC600+(height+4)*2+y*width+x),maplog) if x >= width: y += 1 x = 0 try: level_map[y].append(tile) except IndexError: if settings.warnings == True: #Error handler print "WARNING: Out of range!\n\ There are more bytes than the map can handle.\n\ This is either due to a badly hacked ROM, a bug in the game itself,\n\ or an error in the program's decompression function.\n\ If this results in a buggy map, please report this.\n\ x: {}; y: {}; i: {}; buffer: {}; map width: {}; map height: {}; RAM: {:#x}; ROM: {:#x}\n\ Press Enter to continue or Ctrl+C to quit.".format(x,y,i,buf,len(level_map[0]),len(level_map),0xC600+(height+4)*2+y*width+x,offset) read_map = False byte = 0xEE f.seek(1,1) raw_input() break x += 1 elif nibble == 0xD: #Here, tiles only take up four bits -- useful when lots of tiles in a row are next to each other high_nibble = nib.readnibble() << 4 rom_utilities.write_log("High nibble: {:#x}".format(high_nibble),maplog) length = nib.readnibble(count=2) rom_utilities.write_log("Length: {:#04x} + 0x14 -> {:#x}".format(length,length+0x14),maplog) for i in range(length+0x14): low_nibble = nib.readnibble() tile = high_nibble | low_nibble rom_utilities.write_log("Low nibble: {:#x}; Tile: {:#04x}".format(low_nibble, tile),maplog) buf.append(tile) elif nibble == 0xE: #Same case as before, but for shorter structures (also takes slightly up less space in ROM) high_nibble = nib.readnibble() << 4 rom_utilities.write_log("High nibble: {:#x}".format(high_nibble),maplog) if high_nibble == 0xE0: #We are done!! Stop reading map tiles read_map = False else: length = nib.readnibble() rom_utilities.write_log("Length: {:#x} + 0x4 -> {:#x}".format(length,length+0x4),maplog) for i in range(length+4): low_nibble = nib.readnibble() tile = high_nibble | low_nibble rom_utilities.write_log("Low nibble: {:#x}; Tile: {:#04x}".format(low_nibble, tile),maplog) buf.append(tile) elif nibble == 0xF: #Same tile repeated over and over tile = nib.readnibble(count=2) length = nib.readnibble() rom_utilities.write_log("Tile: {:#04x}".format(tile),maplog) if length >= 8: length = ((length & 0x7) << 4) + nib.readnibble() rom_utilities.write_log("Length: {:#04x} + 0x0b -> {:#04x}".format(length,length+0xb),maplog) for i in range(length+11): buf.append(tile) else: rom_utilities.write_log("Length: {:#x} + 0x3 -> {:#x}".format(length,length+0x3),maplog) for i in range(length+3): buf.append(tile) else: #Normal case!! Uncompressed tile (0x00-0xAF) temp = nibble nibble = nib.readnibble() rom_utilities.write_log("Nibble read: {:#x}".format(nibble),maplog) tile = (temp << 4) + nibble buf.append(tile) rom_utilities.write_log("Uncompressed tile! ({:#04x})".format(tile),maplog) if len(buf) > 1: rom_utilities.write_log("Buffer: {}; x: {}; y: {}; RAM: {:#x} - {:#x}".format(buf,x,y,0xC600+(height+4)*2+y*width+x,0xC5FF+(height+4)*2+y*width+x+len(buf)),maplog) elif len(buf) == 1: rom_utilities.write_log("Buffer: {}; x: {}; y: {}; RAM: {:#x}".format(buf,x,y,0xC600+(height+4)*2+y*width+x),maplog) else: rom_utilities.write_log("x: {}; y: {}; RAM: {:#x}".format(x-1,y,0xC5FF+(height+4)*2+y*width+x),maplog) #Write the tiles for i in buf: if x >= width: y += 1 x = 0 try: level_map[y].append(i) except IndexError: if settings.warnings == True: #Error handler print "WARNING: Out of range!\n\ There are more bytes than the map can handle.\n\ This is either due to a badly hacked ROM, a bug in the game itself,\n\ or an error in the program's decompression function.\n\ If this results in a buggy map, please report this.\n\ x: {}; y: {}; i: {}; buffer: {}; map width: {}; map height: {}; RAM: {:#x}; ROM: {:#x}\n\ Press Enter to continue or Ctrl+C to quit.".format(x,y,i,buf,len(level_map[0]),len(level_map),0xC600+(height+4)*2+y*width+x,offset) read_map = False nib.byte = 0xEE f.seek(1,1) raw_input() break x += 1 buf = [] print "Done decompressing the map tiles." if settings.maplog == True: print "Done writing to log." maplog.close() #print level_map print "Note: Sprites are not supported yet." #Initial code for sprites if nib.byte & 0xF0 == 0xE0 and nib.byte != 0xE0 and nib.byte != 0xEE: #Not sure if this ever actually occurs, but it imitates the game #This function exists because sometimes the EE string ends on a high nibble (e.g. _E E_); sometimes on a low one (e.g. __ EE __) f.seek(-1,1) #Initially, any tile with a sprite is initialized to 0x80. We must find the sprite table to use the correct graphics (and add sprites when that gets implemented) #Sprite pointers take place just after the initial map data, and they are read each time the game finds a tile with value 0x80 or greater for i,v in enumerate(level_map): for j,w in enumerate(level_map[i]): tile_id = w if tile_id == 0xFF: break if rom_utilities.bit_test(tile_id,7) == 1: sprite_id = rom_utilities.readbyte(f) level_map[i][j] = sprite_id sprite_address = rom_utilities.get_abs_ptr(f, 0x10, 0x4000B, level_type) sprite_table = [] #Now the sprite pointers are used to find tuples with the right tile graphics and the sprites (latter is not implemented yet) for i,v in enumerate(level_map): for j,w in enumerate(level_map[i]): sprite_id = w if rom_utilities.bit_test(sprite_id,7) == 1: sprite_id &= 0x7F f.seek(sprite_address + sprite_id*3) tile_id = rom_utilities.readbyte(f) level_map[i][j] = tile_id sprite_id = struct.unpack("<BB",f.read(2)) sprite_table.append((i,j,sprite_id[0],sprite_id[1])) #Used to implement sprites later on!!! return width, height, level_map, sprite_table
level_id = level_ptrs[game][level] print "Internal level ID: {}".format(level_id) print "You have selected: {}.".format(level_names[game][level]) #Get level header data, which is used for various things like the right tiles, water level, etc. level_header = rom_utilities.get_rel_ptr(rom, 0x10, 0x40001, level_id) print "Level header offset: {:#x}".format(level_header) #Get level type (Stilt, River, etc.) rom.seek(level_header+1) level_type = rom_utilities.readbyte(rom) print "Level type: {:#x} ({})".format(level_type,level_types[game][level_type]) #Find address for compressed tiles tile_offset = rom_utilities.get_abs_ptr(rom, 0x10, 0x40005, level_type) print "Compressed tiles are located at: {:#x}".format(tile_offset) raw_tiles, no_tiles = tiles.decompress(rom,tile_offset) #Output tileset as PNG file if settings.tileset == True: imagearea=(no_tiles+0xF)/0x10*0x400 imagewidth=0x80 imageheight=imagearea/imagewidth pixel_map = rom_utilities.decode_tiles(imageheight, imagewidth, raw_tiles, 1) grayscale = True #Could use some cleaning up if grayscale == True: