Created
July 26, 2015 16:08
-
-
Save mmmulani/32692a80e4ffece15b1d to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3.3 | |
import nlzss.lzss3 | |
import pygame | |
import struct | |
import sys | |
from optparse import OptionParser | |
DEBUG_MODE = False | |
def debug(*args, **kwargs): | |
if DEBUG_MODE: | |
print(*args, **kwargs) | |
def main(): | |
parser = OptionParser(usage="usage: %prog pokemon-rom") | |
parser.add_option("-v", "--verbose", | |
help="Print information while running to help debug", action="store_true", | |
dest="verbose") | |
(options, args) = parser.parse_args() | |
global DEBUG_MODE | |
if options.verbose: | |
DEBUG_MODE = True | |
bytes = load_rom(args[0]) | |
strings = load_strings(bytes, '0x3eecfc') | |
debug('Found all these strings: {}'.format(strings)) | |
banks = load_maps(bytes, '0x3526A8') | |
offsets = calculate_map_offsets(banks, 3, 0) | |
min_x = min([x for ((m, b), (x, y)) in offsets]) | |
min_y = min([y for ((m, b), (x, y)) in offsets]) | |
max_x = max([x + banks[m][b]['width'] for ((m, b), (x, y)) in offsets]) | |
max_y = max([y + banks[m][b]['height'] for ((m, b), (x, y)) in offsets]) | |
width = (max_x - min_x) * 16 | |
height = (max_y - min_y) * 16 | |
x_orig = min_x * 16 | |
y_orig = min_y * 16 | |
pygame.init() | |
screen = pygame.display.set_mode((width, height)) | |
screen.fill((255, 255, 255)) | |
for ((m, b), (x, y)) in offsets: | |
draw_map(screen, bytes, banks[m][b]['map_data'], (x - min_x) * 16, (y - min_y) * 16) | |
pygame.display.flip() | |
pygame.image.save(screen, 'wholemap.bmp') | |
pygame.quit() | |
def load_rom(rom_path): | |
return open(rom_path, 'rb').read() | |
def load_strings(bytes, hex_offset): | |
offset = int(hex_offset, 16) | |
table = { | |
0x00:' ',0x01:'À',0x02:'Á',0x03:'Â',0x04:'Ç',0x05:'È',0x06:'É',0x07:'Ê', | |
0x08:'Ë',0x09:'Ì',0x0B:'Î',0x0C:'Ï',0x0D:'Ò',0x0E:'Ó',0x0F:'Ô',0x10:'Œ', | |
0x11:'Ù',0x12:'Ú',0x13:'Û',0x14:'Ñ',0x15:'ß',0x16:'à',0x17:'á',0x19:'ç', | |
0x1A:'è',0x1B:'é',0x1C:'ê',0x1D:'ë',0x1E:'ì',0x20:'î',0x21:'ï',0x22:'ò', | |
0x23:'ó',0x24:'ô',0x25:'œ',0x26:'ù',0x27:'ú',0x28:'û',0x29:'ñ',0x2A:'º', | |
0x2B:'ª',0x2D:'&',0x2E:'+',0x34:'[Lv]',0x35:'=',0x36:';',0x51:'¿',0x52:'¡', | |
0x53:'[pk]',0x54:'[mn]',0x55:'[po]',0x56:'[ké]',0x57:'[bl]',0x58:'[oc]', | |
0x59:'[k]',0x5A:'Í',0x5B:'%',0x5C:'(',0x5D:')',0x68:'â',0x6F:'í',0x79:'[U]', | |
0x7A:'[D]',0x7B:'[L]',0x7C:'[R]',0x85:'<',0x86:'>',0xA1:'0',0xA2:'1', | |
0xA3:'2',0xA4:'3',0xA5:'4',0xA6:'5',0xA7:'6',0xA8:'7',0xA9:'8',0xAA:'9', | |
0xAB:'!',0xAC:'?',0xAD:'.',0xAE:'-',0xAF:'·',0xB0:'...',0xB1:'«',0xB2:'»', | |
0xB3:'\'',0xB4:'\'',0xB5:'|m|',0xB6:'|f|',0xB7:'$',0xB8:',',0xB9:'*', | |
0xBA:'/',0xBB:'A',0xBC:'B',0xBD:'C',0xBE:'D',0xBF:'E',0xC0:'F',0xC1:'G', | |
0xC2:'H',0xC3:'I',0xC4:'J',0xC5:'K',0xC6:'L',0xC7:'M',0xC8:'N',0xC9:'O', | |
0xCA:'P',0xCB:'Q',0xCC:'R',0xCD:'S',0xCE:'T',0xCF:'U',0xD0:'V',0xD1:'W', | |
0xD2:'X',0xD3:'Y',0xD4:'Z',0xD5:'a',0xD6:'b',0xD7:'c',0xD8:'d',0xD9:'e', | |
0xDA:'f',0xDB:'g',0xDC:'h',0xDD:'i',0xDE:'j',0xDF:'k',0xE0:'l',0xE1:'m', | |
0xE2:'n',0xE3:'o',0xE4:'p',0xE5:'q',0xE6:'r',0xE7:'s',0xE8:'t',0xE9:'u', | |
0xEA:'v',0xEB:'w',0xEC:'x',0xED:'y',0xEE:'z',0xEF:'|>|',0xF0:':',0xF1:'Ä', | |
0xF2:'Ö',0xF3:'Ü',0xF4:'ä',0xF5:'ö',0xF6:'ü',0xF7:'|A|',0xF8:'|V|', | |
0xF9:'|<|',0xFA:'|nb|',0xFB:'|nb2|',0xFC:'|FC|',0xFD:'|FD|',0xFE:'|br|', | |
} | |
strings = [] | |
string = '' | |
while True: | |
char = struct.unpack('<B', bytes[offset:(offset + 1)])[0] | |
offset = offset + 1 | |
if char == 0xff: | |
strings.append(string) | |
string = '' | |
continue | |
elif char not in table: | |
break | |
string += table[char] | |
return strings | |
def load_maps(bytes, hex_offset): | |
offset = int(hex_offset, 16) | |
bank_pointers = [] | |
while is_pointer(bytes, offset): | |
bank_pointer = read_pointer(bytes, offset) | |
bank_pointers.append(bank_pointer) | |
offset = offset + 4 | |
debug('Found these bank pointers: {}'.format(bank_pointers)) | |
banks = [] | |
for i, bank_pointer in enumerate(bank_pointers): | |
offset = bank_pointer | |
next_pointer = bank_pointers[i + 1] if (i + 1) < len(bank_pointers) else 0 | |
if i == 42: | |
break | |
maps = [] | |
while is_pointer(bytes, offset): | |
map_data = read_pointer(bytes, offset) | |
cs = read_connections(bytes, map_data) | |
map_pointer = read_pointer(bytes, map_data) | |
width = read_int(bytes, map_pointer) | |
height = read_int(bytes, map_pointer + 4) | |
maps.append({ | |
'map_data': map_data, | |
'connections': cs, | |
'width': width, | |
'height': height, | |
}) | |
offset = offset + 4 | |
if offset == next_pointer: | |
break | |
debug('Found {} map pointers: {}'.format(len(maps), maps)) | |
banks.append(maps) | |
return banks | |
def read_connections(bytes, map_data): | |
cs = [] | |
if not is_pointer(bytes, map_data + 12): | |
return cs | |
connections = read_pointer(bytes, map_data + 12) | |
n_connections = read_int(bytes, connections) | |
offset = read_pointer(bytes, connections + 4) | |
debug('Reading connections at {:#x}'.format(offset)) | |
for i in range(n_connections): | |
cs.append({ | |
'direction': read_int(bytes, offset), | |
'offset': read_uint(bytes, offset + 4), | |
'map_bank': struct.unpack('<B', bytes[(offset + 8):(offset + 9)])[0], | |
'map_number': struct.unpack('<B', bytes[(offset + 9):(offset + 10)])[0], | |
}) | |
offset += 12 | |
return cs | |
def read_map(bytes, header_pointer): | |
map_pointer = read_pointer(bytes, header_pointer) | |
width = read_int(bytes, map_pointer) | |
height = read_int(bytes, map_pointer + 4) | |
label = struct.unpack('<B', bytes[(header_pointer + 20):(header_pointer + 21)])[0] | |
border = read_pointer(bytes, map_pointer + 8) | |
tiles_pointer = read_pointer(bytes, map_pointer + 12) | |
tileset_pointer = read_pointer(bytes, map_pointer + 16) | |
local_pointer = read_pointer(bytes, map_pointer + 20) | |
debug('Map at {}'.format(map_pointer)) | |
debug('Width/height: {}/{}'.format(width, height)) | |
debug('Border pointer: {0:#x}, tiles pointer: {1:#x}'.format( | |
border, tiles_pointer)) | |
tile_sprites = {} | |
offset = tiles_pointer | |
i = 0 | |
for y in range(height): | |
for x in range(width): | |
tile_data = \ | |
struct.unpack('<H', bytes[(offset + i * 2):(offset + (i + 1) * 2)])[0] | |
attribute = tile_data >> 10 | |
tile = tile_data & 0x3ff | |
debug('tile at ({}, {}): {:#x}, attribute: {:#x}'.format( | |
x, y, tile, attribute)) | |
tile_sprites[(x, y)] = tile | |
i = i + 1 | |
return (width, height, label, tile_sprites, tileset_pointer, local_pointer) | |
def read_tileset(bytes, tileset_pointer): | |
attribs = struct.unpack('<2B', bytes[tileset_pointer:(tileset_pointer + 2)]) | |
debug('Tileset compressed: {}, primary: {}'.format(attribs[0], attribs[1])) | |
primary = attribs[1] | |
tileset_image_pointer = read_pointer(bytes, tileset_pointer + 4) | |
image = nlzss.lzss3.decompress_bytes(bytes[tileset_image_pointer:]) | |
tiles = [] | |
for i in range(0, len(image), 32): | |
tile = [] | |
for j in range(64): | |
px = image[int(i + (j / 2))] | |
if j % 2 == 0: | |
px = px & 0xf | |
else: | |
px = int(px / 0x10) | |
tile.append(px) | |
tiles.append(tile) | |
debug('Total number of tiles read: {}'.format(len(tiles))) | |
offset = read_pointer(bytes, tileset_pointer + 8) | |
debug('Palette pointer: {:#x}'.format(offset)) | |
palette_range = range(7) if primary == 0 else range(7, 16) | |
palettes = [] | |
for i in palette_range: | |
palette = [] | |
for j in range(16): | |
colours = \ | |
struct.unpack('<H', | |
bytes[(offset + (i * 32) + (j * 2)):\ | |
(offset + (i * 32) + ((j + 1) * 2))])[0] | |
(r, g, b) = (colours & 0x1f, (colours >> 5) & 0x1f, colours >> 10) | |
(r, g, b) = (r * 8, g * 8, b * 8) | |
palette.append((r, g, b)) | |
palettes.append(palette) | |
offset = read_pointer(bytes, tileset_pointer + 12) | |
end = read_pointer(bytes, tileset_pointer + 20) | |
total_blocks = (end - offset) / 16 | |
debug('trying to read {} blocks'.format(total_blocks)) | |
blocks = [] | |
for i in range(int(total_blocks)): | |
blocks.append(read_block(bytes, offset, i)) | |
return (palettes, tiles, blocks) | |
def read_second_blocks(bytes, header_pointer): | |
map_pointer = read_pointer(bytes, header_pointer) | |
tileset_pointer = read_pointer(bytes, map_pointer + 20) | |
offset = read_pointer(bytes, tileset_pointer + 12) | |
blocks = [] | |
for i in range(96): | |
b = read_block(bytes, offset, i) | |
blocks.append(b) | |
return blocks | |
def read_block(bytes, offset, i): | |
block = [] | |
for j in range(8): | |
block_data = \ | |
struct.unpack('<H', bytes[(offset + i * 16 + j * 2):(offset + i * 16 + (j + 1) * 2)])[0] | |
palette = block_data >> 12 | |
tile = block_data & 0x3ff | |
attributes = (block_data >> 10) & 0x3 | |
block.append((palette, tile, attributes)) | |
return block | |
def draw_block(screen, palettes, tiles, blocks, x, y, block_num): | |
# The first four tiles are the bottom tiles and the last four are the top | |
# ones. The top tiles also have a mask to them, so we have to draw them | |
# differently. | |
block = blocks[block_num] | |
for i, (palette, tile, attributes) in enumerate(block): | |
x_offset = (i % 2) * 8 | |
y_offset = int((i % 4) / 2) * 8 | |
draw_tile(screen, palettes[palette], tiles[tile], | |
x + x_offset, y + y_offset, attributes, i >= 4) | |
def draw_tile(screen, palette, tile, x, y, attributes, mask_mode): | |
x_flip = attributes & 0x1 | |
y_flip = attributes & 0x2 | |
for i, px in enumerate(tile): | |
x_offset = (i % 8) | |
if x_flip: | |
x_offset = 8 - (x_offset + 1) | |
y_offset = int(i / 8) | |
if y_flip: | |
y_offset = 8 - (y_offset + 1) | |
if mask_mode and px == 0: | |
continue | |
colour = palette[px] | |
screen.set_at((x + x_offset, y + y_offset), colour) | |
def draw_and_save_map(screen, bytes, map_, strings): | |
screen.fill((255, 255, 255)) | |
label = draw_map(screen, bytes, map_, 0, 0) | |
pygame.display.flip() | |
name = strings[label - 88] | |
pygame.image.save(screen, 'maps/{}.bmp'.format(name)) | |
def draw_map(screen, bytes, map_, xx, yy): | |
(width, height, label, tile_sprites, global_pointer, local_pointer) = read_map(bytes, map_) | |
(palettes, tiles, blocks) = read_tileset(bytes, global_pointer) | |
(extra_palettes, extra_tiles, extra_blocks) = read_tileset(bytes, local_pointer) | |
palettes.extend(extra_palettes) | |
tiles.extend(extra_tiles) | |
blocks.extend(extra_blocks) | |
for (x, y) in tile_sprites: | |
draw_block(screen, palettes, tiles, blocks, xx + x * 16, yy + y * 16, tile_sprites[(x, y)]) | |
return label | |
# Returns a set of maps and (x, y) coordinates that the maps should be drawn at. | |
# The (x, y) coordinates are specified in blocks, not pixels. | |
# Coordinates originate from the top-left of a map. | |
def calculate_map_offsets(maps, bank_num, map_num): | |
visited = {} | |
def calculate_helper(map_id, caller_id, caller_offset, direction, offset): | |
if map_id in visited: | |
return [] | |
(bank, map_) = map_id | |
(c_bank, c_map) = caller_id | |
width = maps[bank][map_]['width'] | |
height = maps[bank][map_]['height'] | |
c_width = maps[c_bank][c_map]['width'] | |
c_height = maps[c_bank][c_map]['height'] | |
visited[map_id] = True | |
(caller_x, caller_y) = caller_offset | |
if direction == 0x1: # Down | |
coord = (caller_x + offset, caller_y + c_height) | |
elif direction == 0x2: # Up | |
coord = (caller_x + offset, caller_y - height) | |
elif direction == 0x3: # Left | |
coord = (caller_x - width, caller_y + offset) | |
elif direction == 0x4: # Right | |
coord = (caller_x + c_width, caller_y + offset) | |
elif direction == 0xf: # Special starting value | |
coord = (caller_x, caller_y) | |
else: | |
return [] | |
coords = [(map_id, coord)] | |
for c in maps[bank][map_]['connections']: | |
coords.extend(calculate_helper( | |
(c['map_bank'], c['map_number']), | |
map_id, | |
coord, | |
c['direction'], | |
c['offset'], | |
)) | |
return coords | |
return calculate_helper( | |
(bank_num, map_num), | |
(bank_num, map_num), | |
(0, 0), 0xf, 0) | |
def is_pointer(bytes, offset): | |
return read_pointer(bytes, offset) > 0 | |
def read_pointer(bytes, offset): | |
load_address = int('0x8000000', 16) | |
return read_int(bytes, offset) - load_address | |
def read_uint(bytes, offset): | |
return struct.unpack(b'<i', bytes[offset:(offset + 4)])[0] | |
def read_int(bytes, offset): | |
return struct.unpack(b'<I', bytes[offset:(offset + 4)])[0] | |
if __name__ == '__main__': | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment