diff --git a/ff5reader.py b/ff5reader.py index af2674a..abe029c 100755 --- a/ff5reader.py +++ b/ff5reader.py @@ -97,19 +97,6 @@ if pyqt_version == 0: 'Make sure you installed the PyQt4 package.') sys.exit(-1) -with open(filename_en, 'rb') as file: - ROM_en = file.read() -print(len(ROM_en), filename_en) -with open(filename_jp, 'rb') as file: - ROM_jp = file.read() -print(len(ROM_jp), filename_jp) -with open(filename_jp_ff4, 'rb') as file: - ROM_FF4jp = file.read() -print(len(ROM_FF4jp), filename_jp_ff4) -with open(filename_jp_ff6, 'rb') as file: - ROM_FF6jp = file.read() -print(len(ROM_FF6jp), filename_jp_ff6) - class FF5Reader(QMainWindow): ''' @@ -118,6 +105,21 @@ class FF5Reader(QMainWindow): def __init__(self): QMainWindow.__init__(self, None) perfcount() + print('Reading ROMs') + with open(filename_en, 'rb') as file: + ROM_en = file.read() + print(len(ROM_en), filename_en) + with open(filename_jp, 'rb') as file: + ROM_jp = file.read() + print(len(ROM_jp), filename_jp) + with open(filename_jp_ff4, 'rb') as file: + ROM_FF4jp = file.read() + print(len(ROM_FF4jp), filename_jp_ff4) + with open(filename_jp_ff6, 'rb') as file: + ROM_FF6jp = file.read() + print(len(ROM_FF6jp), filename_jp_ff6) + perfcount() + print('Generating Glyphs') self.glyph_sprites = { 'glyphs_en_s': generate_glyphs(ROM_en, 0x11F000), @@ -126,7 +128,7 @@ class FF5Reader(QMainWindow): 'glyphs_jp_l': generate_glyphs_large(ROM_jp, 0x03E800), 'glyphs_kanji': generate_glyphs_large(ROM_jp, 0x1BD000, 0x1AA), # Kanji are unchanged in EN version } - make_string_img_list = functools.partial(_make_string_img_list, **self.glyph_sprites) + make_string_img_list = functools.partial(_make_string_img_list, ROM_en, ROM_jp, **self.glyph_sprites) perfcount() stringlist_headers = ['Address', 'ID', 'Name'] @@ -161,7 +163,7 @@ class FF5Reader(QMainWindow): zone_structure = [('NPC Layer', 2, None), # 00 01 ('Name', 1, [z[2] for z in zone_names]), # 02 ('ShadowFlags', 1, None), # 03 - ('Graphic maths', 1, None), # 04 + ('Graphic maths', 1, None), # 04 - MSb animation-related, 6 LSbs are ID for table in 0x005BB8 which writes to $2131-$2133 (Color Math Designation, Subscreen BG color) ('Tile properties', 1, None), # 05 ('Flags '+hex(6), 1, None), # 06 (hex(7), 1, None), # 07 @@ -255,11 +257,14 @@ class FF5Reader(QMainWindow): blockmaps = get_blockmaps(ROM_jp) field_blocks = [] zone_pxs = [] + block_cache = {'misses': 0, 'p_hits': 0, 'i_hits': 0} + zone_px_cache = {'misses': 0, 'hits': 0} for z in zones: - blocks = make_field_map_blocks_px(ROM_jp, z, field_tiles, field_minitiles, field_blocksets) - field_blocks.append(stitch_tileset_px([b.all for b in blocks])) - #zone_pxs += make_zone_pxs(blocks, [blockmaps[b] for b in z.blockmaps if b!=-1]) - zone_pxs += make_zone_pxs2(blocks, blockmaps, z) + blocks, miniblocks = make_field_map_blocks_px(ROM_jp, z, field_tiles, field_minitiles, field_blocksets, block_cache) + field_blocks.append(stitch_tileset_px([b.all for b in blocks+miniblocks])) + zone_pxs += make_zone_pxs(blocks, miniblocks, blockmaps, z, zone_px_cache) + print('Block cache results: {misses} misses, {p_hits} full hits, {i_hits} palette misses'.format(**block_cache)) + print('Zone pixmap cache results: {misses} misses, {hits} hits'.format(**zone_px_cache)) perfcount() print('Generating Battle backgrounds') @@ -464,7 +469,7 @@ def make_string_img_large(bytestring, glyphs, glyphs_kanji, macros=None, jp=Fals return string, QPixmap.fromImage(img.copy(0, 0, xmax, y+16)) -def _make_string_img_list(start, length, num, +def _make_string_img_list(rom_e, rom_j, start, length, num, start_jp=None, len_jp=None, start_str=None, start_jp_str=None, indirect=False, large=False, macros_en=None, macros_jp=None, glyphs_en_s=None, glyphs_en_l=None, @@ -480,13 +485,13 @@ def _make_string_img_list(start, length, num, for id in range(num): en = start + (id*length) jp = start_jp + (id*len_jp) - en_start = int.from_bytes(ROM_en[en:en+length],'little') + start_str - en_end = int.from_bytes(ROM_en[en+length:en+(length*2)],'little') + start_str + en_start = int.from_bytes(rom_e[en:en+length],'little') + start_str + en_end = int.from_bytes(rom_e[en+length:en+(length*2)],'little') + start_str if en_start >= 0xC00000: # SNES memory space has the ROM starting at 0xC00000 in HiROM mode. en_start -= 0xC00000 en_end -= 0xC00000 - jp_start = int.from_bytes(ROM_jp[jp:jp+len_jp],'little') + start_jp_str - jp_end = int.from_bytes(ROM_jp[jp+len_jp:jp+(len_jp*2)],'little') + start_jp_str + jp_start = int.from_bytes(rom_j[jp:jp+len_jp],'little') + start_jp_str + jp_end = int.from_bytes(rom_j[jp+len_jp:jp+(len_jp*2)],'little') + start_jp_str if jp_start >= 0xC00000: # SNES memory space has the ROM starting at 0xC00000 in HiROM mode. jp_start -= 0xC00000 jp_end -= 0xC00000 @@ -494,17 +499,17 @@ def _make_string_img_list(start, length, num, break try: # When dealing with pointer redirection we might end up passing empty strings if large: - str_en, img_en = make_string_img_large(ROM_en[en_start:en_end], glyphs_en_l, glyphs_kanji, macros_en) + str_en, img_en = make_string_img_large(rom_e[en_start:en_end], glyphs_en_l, glyphs_kanji, macros_en) else: - str_en, img_en = make_string_img_small(ROM_en[en_start:en_end], glyphs_en_s) + str_en, img_en = make_string_img_small(rom_e[en_start:en_end], glyphs_en_s) except ValueError: str_en = '' img_en = None try: if large: - str_jp, img_jp = make_string_img_large(ROM_jp[jp_start:jp_end], glyphs_jp_l, glyphs_kanji, macros_jp, jp=True) + str_jp, img_jp = make_string_img_large(rom_j[jp_start:jp_end], glyphs_jp_l, glyphs_kanji, macros_jp, jp=True) else: - str_jp, img_jp = make_string_img_small(ROM_jp[jp_start:jp_end], glyphs_jp_s, jp=True) + str_jp, img_jp = make_string_img_small(rom_j[jp_start:jp_end], glyphs_jp_s, jp=True) except ValueError: str_jp = '' img_jp = None @@ -517,11 +522,11 @@ def _make_string_img_list(start, length, num, j1 = start + (id*length) j2 = start_jp + (id*len_jp) if large: - str_en, img_en = make_string_img_large(ROM_en[j1:j1+length], glyphs_en_l, glyphs_kanji, macros_en) - str_jp, img_jp = make_string_img_large(ROM_jp[j2:j2+len_jp], glyphs_jp_l, glyphs_kanji, macros_jp, jp=True) + str_en, img_en = make_string_img_large(rom_e[j1:j1+length], glyphs_en_l, glyphs_kanji, macros_en) + str_jp, img_jp = make_string_img_large(rom_j[j2:j2+len_jp], glyphs_jp_l, glyphs_kanji, macros_jp, jp=True) else: - str_en, img_en = make_string_img_small(ROM_en[j1:j1+length], glyphs_en_s) - str_jp, img_jp = make_string_img_small(ROM_jp[j2:j2+len_jp], glyphs_jp_s, jp=True) + str_en, img_en = make_string_img_small(rom_e[j1:j1+length], glyphs_en_s) + str_jp, img_jp = make_string_img_small(rom_j[j2:j2+len_jp], glyphs_jp_s, jp=True) stringlist.append([hex(j1, 6), hex(id, id_digits), str_en, img_en, str_jp, img_jp]) return stringlist diff --git a/includes/snes.py b/includes/snes.py index 15fe8d1..0f11751 100644 --- a/includes/snes.py +++ b/includes/snes.py @@ -22,8 +22,9 @@ from includes.helpers import * from includes.snestile import * from collections import namedtuple -Zone = namedtuple('Zone', 'npcs name shadowflags blockset tilesets blockmaps palette music') +Zone = namedtuple('Zone', 'npcs name shadowflags blockset tilesets blockmaps pal palette music') Block = namedtuple('Block', 'priority0 priority1 all') +TileMapping = namedtuple('TileMapping', 'tile palette h_flip v_flip priority') def parse_zone(rom, id, start_address=0x0E9C00): ptr = start_address+(id*0x1A) @@ -40,10 +41,22 @@ def parse_zone(rom, id, start_address=0x0E9C00): blockmaps = [(blockmaps_b & 0x000003FF) - 1, ((blockmaps_b & 0x000FFC00) >> 10) - 1, ((blockmaps_b & 0x3FF00000) >> 20) - 1] - pal_address = 0x03BB00 + (rom[ptr+0x16]<<8) + pal = rom[ptr+0x16] + pal_address = 0x03BB00 + (pal<<8) palettes = [generate_palette(rom, pal_address+i*32, transparent=True) for i in range(8)] music = rom[ptr+0x19] - return Zone(npcs, name, shadowflags, blockset, tilesets, blockmaps, palettes, music) + return Zone(npcs, name, shadowflags, blockset, tilesets, blockmaps, pal, palettes, music) + +''' +This is a bit of a mess of pointer chains for now, so generalising it will have to wait. +UPDATE: i2-i7 merely obtain a zone ID. Whoops. +''' +#i2 = indirect(rom, 0x0E2400 + id*2) +#i3 = indirect(rom, 0x0E2402 + i2)*2 +#i4 = indirect(rom, 0x18E080 + i3) +#i5 = indirect(rom, 0x18E081 + i4+4)*3 +#i6 = indirect(rom, 0x083320 + i5) +#i7 = indirect(rom, 0x080001 + i6) & 0x03FF def make_battle_strip(rom, palette_address, tile_address, num_tiles, bpp=4): @@ -204,36 +217,14 @@ def stitch_tileset_px(tiles_px): canvas.draw_pixmap(i%16, i//16, tile) return canvas.pixmap() -def get_field_map_tiles(rom, id): - ''' - This is a bit of a mess of pointer chains for now, so generalising it will have to wait. - Palette selection is probably determined by the tilemap which is outside the scope of this, so we'll just use #1. - UPDATE: i2-i7 merely obtain a zone ID. Whoops. - ''' - #i2 = indirect(rom, 0x0E2400 + id*2) - #i3 = indirect(rom, 0x0E2402 + i2)*2 - #i4 = indirect(rom, 0x18E080 + i3) - #i5 = indirect(rom, 0x18E081 + i4+4)*3 - #i6 = indirect(rom, 0x083320 + i5) - #i7 = indirect(rom, 0x080001 + i6) & 0x03FF - i8 = id * 0x1A - tilesets = indirect(rom, 0x0E9C09 + i8, length=3) - tile_index_0 = (tilesets & 0x00003F) # (indirect(rom, 0x0E9C09 + i8) & 0x003F) - tile_index_1 = (tilesets & 0x000FC0) >> 6 # (indirect(rom, 0x0E9C09 + i8) & 0x0FC0)>>6 - tile_index_2 = (tilesets & 0x03F000) >> 12 # (indirect(rom, 0x0E9C0A + i8) & 0x03F0)>>4 - minitile_index = (tilesets & 0xFC0000) >> 18 # (indirect(rom, 0x0E9C0A + i8) & 0x03F0)>>4 - pal_offset = indirect(rom, 0x0E9C16 + i8) * 0x100 - palette_address = 0x03BB00 + pal_offset - palettes = [generate_palette(rom, palette_address+i*32, transparent=True) for i in range(8)] - return tile_index_0, tile_index_1, tile_index_2, minitile_index, palettes - def make_field_map_tile_pixmap(rom, id, st_tiles, st_minitiles): - *tiles, minitile, palettes = get_field_map_tiles(rom, id) - p = palettes[1] + #*tiles, minitile, palettes = get_field_map_tiles(rom, id) + zone = parse_zone(rom, id) + p = zone.palette[1] canvas = Canvas(16, 64) - for i, ts in enumerate(tiles): + for i, ts in enumerate(zone.tilesets[:-1]): canvas.draw_pixmap(0, i*16, st_tiles[ts].pixmap(p)) - canvas.draw_pixmap(0, 48, st_minitiles[minitile].pixmap(p)) + canvas.draw_pixmap(0, 48, st_minitiles[zone.tilesets[-1]].pixmap(p)) return canvas.pixmap() def get_field_map_block_layouts(rom, id, start_address=0x0F0000): @@ -244,26 +235,42 @@ def get_field_map_block_layouts(rom, id, start_address=0x0F0000): output.append([data[j+i+k] for j in range(0, 0x800, 0x200) for k in range(2)]) return output -def make_field_map_blocks_px(rom, zone, tilesets, minitilesets, blocksets): - *i_tiles, i_minitiles = zone.tilesets - tiles = tilesets[i_tiles[0]] + tilesets[i_tiles[1]] + tilesets[i_tiles[2]] - tiles += minitilesets[i_minitiles] - blockset = blocksets[zone.blockset] - blocks = [Block(*[b.pixmap(zone.palette) for b in make_block(tm, tiles)]) for tm in blockset] - return blocks +def make_field_map_blocks_px(rom, zone, tilesets, minitilesets, blocksets, cache): + cache_key_i = '{} {}'.format(' '.join([str(i) for i in zone.tilesets]), zone.blockset) + cache_key_p = '{} {}'.format(cache_key_i, zone.pal) + if cache_key_p in cache: + cache['p_hits'] += 1 + return cache[cache_key_p] + elif cache_key_i in cache: + cache['i_hits'] += 1 + blocks, miniblocks = cache[cache_key_i] + else: + cache['misses'] += 1 + *i_tiles, i_minitiles = zone.tilesets + tiles = tilesets[i_tiles[0]] + tilesets[i_tiles[1]] + tilesets[i_tiles[2]] + minitiles = minitilesets[i_minitiles] + blockset = blocksets[zone.blockset] + blockset_parsed = [[parse_tileset_word(tm[i*2:(i+1)*2]) for i in range(len(tm)//2)] for tm in blockset] + blocks = [Block(*make_block(bs, tiles)) for bs in blockset_parsed] + miniblocks = [Block(*make_block(bs, minitiles, tile_modulo=len(minitiles))) for bs in blockset_parsed] + cache[cache_key_i] = (blocks, miniblocks) + + blocks_px = [Block(*[i.pixmap(zone.palette) for i in b]) for b in blocks] + miniblocks_px = [Block(*[i.pixmap(zone.palette) for i in b]) for b in miniblocks] + cache[cache_key_p] = (blocks_px, miniblocks_px) + return blocks_px, miniblocks_px def make_block(tilemap, tiles, cols=2, rows=2, tile_adjust=0, pal_adjust=0, tile_modulo=0x1000): canvases = (Canvas_Indexed(cols, rows), Canvas_Indexed(cols, rows), Canvas_Indexed(cols, rows)) - for i in range(len(tilemap)//2): - tile_index, p, h_flip, v_flip, priority = parse_tileset_word(tilemap[i*2:(i+1)*2]) + for i, tm in enumerate(tilemap): x = i % cols y = i //cols try: - tile = tiles[(tile_index+tile_adjust)%tile_modulo] - canvases[priority].draw_tile(x, y, tile, h_flip, v_flip, p+pal_adjust) - canvases[2].draw_tile(x, y, tile, h_flip, v_flip, p+pal_adjust) + tile = tiles[(tm.tile+tile_adjust)%tile_modulo] + canvases[tm.priority].draw_tile(x, y, tile, tm.h_flip, tm.v_flip, tm.palette+pal_adjust) + canvases[2].draw_tile(x, y, tile, tm.h_flip, tm.v_flip, tm.palette+pal_adjust) except BaseException as e: - print(e, p, hex(tile_index,2), hex(tile_adjust,2), hex(tile_index+tile_adjust,2)) + print(e, tm.palette, hex(tm.tile,2), hex(tile_adjust,2), hex(tm.tile+tile_adjust,2)) return canvases def get_blockmaps(rom, start_address=0x0B0000, num=0x148): @@ -277,36 +284,36 @@ def get_blockmaps(rom, start_address=0x0B0000, num=0x148): blockmaps = [decompress_lzss(rom, ptr) for ptr in ptrs] return blockmaps -def make_zone_pxs(blocks, blockmaps): - output = [] - for bm in blockmaps: +def make_zone_pxs(blocks, miniblocks, blockmaps, zone, cache): + cache_key = '{} {} {}'.format(' '.join([str(i) for i in zone.blockmaps+zone.tilesets]), zone.blockset, zone.pal) + if cache_key in cache: + cache['hits'] += 1 + return cache[cache_key] + else: + cache['misses'] += 1 + output = [] + layers = [None, None, None, None, None, None] # bg1.0 bg1.1 bg2.0 bg2.1 bg3.0 bg3.1 + order = [4, 2, 0, 3, 1, 5] # Draw order from http://problemkaputt.de/fullsnes.htm#snespictureprocessingunitppu + for i, i_b in enumerate(zone.blockmaps): + if i_b == -1: + output.append(None) + else: + canvases = (Canvas(64, 64, tilesize=16), Canvas(64, 64, tilesize=16), Canvas(64, 64, tilesize=16)) + _blocks = blocks if i < 2 else miniblocks + for j, b in enumerate(blockmaps[i_b]): + block = _blocks[b] + canvases[0].draw_pixmap(j%64, j//64, block.priority0) + canvases[1].draw_pixmap(j%64, j//64, block.priority1) + canvases[2].draw_pixmap(j%64, j//64, block.all) + output.append(canvases[2].pixmap()) + layers[i*2:(i+1)*2] = canvases[0:2] canvas = Canvas(64, 64, tilesize=16) - for i, b in enumerate(bm): - canvas.draw_pixmap(i%64, i//64, blocks[b]) + for i in order: + if layers[i]: + canvas.draw_pixmap(0, 0, layers[i].pixmap()) output.append(canvas.pixmap()) - return output - -def make_zone_pxs2(blocks, blockmaps, zone): - output = [] - layers = [None, None, None, None, None, None] # bg1.0 bg1.1 bg2.0 bg2.1 bg3.0 bg3.1 - order = [4, 2, 0, 3, 1, 5] # Draw order from http://problemkaputt.de/fullsnes.htm#snespictureprocessingunitppu - for i, i_b in enumerate(zone.blockmaps): - if i_b == -1: - output.append(None) - else: - canvases = (Canvas(64, 64, tilesize=16), Canvas(64, 64, tilesize=16), Canvas(64, 64, tilesize=16)) - for j, b in enumerate(blockmaps[i_b]): - canvases[0].draw_pixmap(j%64, j//64, blocks[b].priority0) - canvases[1].draw_pixmap(j%64, j//64, blocks[b].priority1) - canvases[2].draw_pixmap(j%64, j//64, blocks[b].all) - output.append(canvases[2].pixmap()) - layers[i*2:(i+1)*2] = canvases[0:2] - canvas = Canvas(64, 64, tilesize=16) - for i in order: - if layers[i]: - canvas.draw_pixmap(0, 0, layers[i].pixmap()) - output.append(canvas.pixmap()) - return output + cache[cache_key] = output + return output def decompress_battle_tilemap(rom, address): ''' @@ -371,7 +378,7 @@ def parse_tileset_word(data): priority = (b & 0x20) >> 5 h_flip = (b & 0x40) >> 6 v_flip = (b & 0x80) >> 7 - return tile_index, palette, h_flip, v_flip, priority + return TileMapping(tile_index, palette, h_flip, v_flip, priority) def make_tilemap_canvas(tilemap, tiles, cols=64, rows=64, tile_adjust=0, pal_adjust=-1, tile_modulo=0x80): ''' @@ -380,7 +387,7 @@ def make_tilemap_canvas(tilemap, tiles, cols=64, rows=64, tile_adjust=0, pal_adj ''' canvas = Canvas_Indexed(cols, rows) for i in range(len(tilemap)//2): - tile_index, p, h_flip, v_flip, priority = parse_tileset_word(tilemap[i*2:(i+1)*2]) + tm = parse_tileset_word(tilemap[i*2:(i+1)*2]) if cols > 32: x = (i % 32) + 32*((i//1024) % 2) y = (i //32) - 32*((i//1024) % 2) @@ -388,10 +395,10 @@ def make_tilemap_canvas(tilemap, tiles, cols=64, rows=64, tile_adjust=0, pal_adj x = i % cols y = i //cols try: - tile = tiles[(tile_index+tile_adjust)%tile_modulo] - canvas.draw_tile(x, y, tile, h_flip, v_flip, p+pal_adjust) + tile = tiles[(tm.tile+tile_adjust)%tile_modulo] + canvas.draw_tile(x, y, tile, tm.h_flip, tm.v_flip, tm.palette+pal_adjust) except BaseException as e: - print(e, p, hex(tile_index,2), hex(tile_adjust,2), hex(tile_index+tile_adjust,2)) + print(e, tm.palette, hex(tm.tile,2), hex(tile_adjust,2), hex(tm.tile+tile_adjust,2)) return canvas def make_tilemap_pixmap(tilemap, tiles, palettes, cols=64, rows=64, tile_adjust=0, pal_adjust=-1): diff --git a/includes/snestile.py b/includes/snestile.py index dc17469..bfa5473 100644 --- a/includes/snestile.py +++ b/includes/snestile.py @@ -254,12 +254,12 @@ class Canvas_Indexed: self.max_col = 1 self.max_row = 1 - def draw_tile(self, col, row, image, h_flip=False, v_flip=False, palette=0): + def draw_tile(self, col, row, image, h_flip=False, v_flip=False, palette=0, bpp=4): image = image.mirrored(h_flip, v_flip) imgbits = image.bits() imgbits.setsize(image.byteCount()) if palette: - p = palette<<4 + p = palette<