From 995be34e0020e9bb477238971003be085f36f7d8 Mon Sep 17 00:00:00 2001 From: Luke Hubmayer-Werner Date: Sat, 24 Mar 2018 22:53:18 +1030 Subject: [PATCH] Some battle backgrounds working. TODO: fix the terrain tile flip patterns, terrain load edge case. --- ff5reader.py | 208 +++++++++++++++++++++++++++++++++++++++++++++++---- snestile.py | 12 ++- 2 files changed, 204 insertions(+), 16 deletions(-) diff --git a/ff5reader.py b/ff5reader.py index 4e5bfba..1158daf 100755 --- a/ff5reader.py +++ b/ff5reader.py @@ -170,11 +170,18 @@ class FF5Reader(QMainWindow): (hex(24, 2), 1, None), ('Music', 1, const.BGM_Tracks)] zone_headers = ['Address'] + [z[0] for z in zone_structure] + zone_data = [parse_struct(ROM_en, 0x0E9C00 + (i*0x1A), zone_structure) for i in range(const.zone_count)] - zone_data = [] - for i in range(const.zone_count): - offset = 0x0E9C00 + (i*0x1A) - zone_data.append(parse_struct(ROM_en, offset, zone_structure)) + battle_bg_structure = [('ImageID', 1, None), + ('ColorID 1', 1, None), + ('ColorID 2', 1, None), + ('TerrainID', 1, None), + ('TerrainFlipID', 1, None), + (hex(5, 1), 1, None), + ('Movement', 1, None), + (hex(7, 1), 1, None),] + battle_bg_headers = ['Address'] + [z[0] for z in battle_bg_structure] + battle_bg_data = [parse_struct(ROM_jp, 0x14BA21 + (i*8), battle_bg_structure) for i in range(34)] tileset_headers = ("ID", "Offset", "Pointer", "Expected Length") tileset_data = [] @@ -209,16 +216,21 @@ class FF5Reader(QMainWindow): perfcount() print('Generating map tiles') - world_tiles = [make_worldmap_tiles(ROM_jp, 0x0FF0C0+(i*0x300), 0x1B8000+(i*0x2000), 0x0FF9C0+(i*0x100), length=l) for i, l in enumerate([256, 256, 256])] - worldpixmaps = [make_worldmap_pixmap(ROM_jp, 0, 0x0FFCC0+(0*0x100), world_tiles[0]), - make_worldmap_pixmap(ROM_jp, 1, 0x0FFCC0+(1*0x100), world_tiles[1]), - make_worldmap_pixmap(ROM_jp, 2, 0x0FFCC0+(0*0x100), world_tiles[0]), - make_worldmap_pixmap(ROM_jp, 3, 0x0FFCC0+(2*0x100), world_tiles[2]), - make_worldmap_pixmap(ROM_jp, 4, 0x0FFCC0+(2*0x100), world_tiles[2])] + worldmap_palettes = [generate_palette(ROM_jp, 0x0FFCC0+(i*0x100), length=0x160, transparent=True) for i in range(3)] - worldmap_tiles = make_worldmap_subtiles_pixmap(ROM_jp, 0x1B8000, 0x0FF9C0, 0x0FFCC0) - worldmap_tiles += make_worldmap_subtiles_pixmap(ROM_jp, 0x1BA000, 0x0FFAC0, 0x0FFDC0) - worldmap_tiles += make_worldmap_subtiles_pixmap(ROM_jp, 0x1BC000, 0x0FFBC0, 0x0FFEC0, length=128) + world_tiles = [make_worldmap_tiles(ROM_jp, 0x0FF0C0+(i*0x300), 0x1B8000+(i*0x2000), 0x0FF9C0+(i*0x100)) for i in range(3)] + worldpixmaps = [make_worldmap_pixmap(ROM_jp, i, 0x0FFCC0+(t*0x100), world_tiles[t]) for i, t in enumerate([0, 1, 0, 2, 2])] + world_tiles_pixmaps = [] + for i, tiles in enumerate(world_tiles): + a = [] + for t in tiles: + t.setColorTable(worldmap_palettes[i]) + a.append(QPixmap.fromImage(t)) + world_tiles_pixmaps.append(a) + perfcount() + worldmap_subtiles = make_worldmap_subtiles_pixmap(ROM_jp, 0x1B8000, 0x0FF9C0, 0x0FFCC0) + worldmap_subtiles += make_worldmap_subtiles_pixmap(ROM_jp, 0x1BA000, 0x0FFAC0, 0x0FFDC0) + worldmap_subtiles += make_worldmap_subtiles_pixmap(ROM_jp, 0x1BC000, 0x0FFBC0, 0x0FFEC0, length=128) perfcount() field_tiles = make_all_field_tiles(ROM_jp) field_minitiles = make_all_field_minitiles(ROM_jp) @@ -228,6 +240,9 @@ class FF5Reader(QMainWindow): perfcount() fieldmap_tiles = [make_field_map_tile_pixmap(ROM_jp, i, st_field_tiles, st_field_minitiles) for i in range(const.zone_count)] perfcount() + print('Generating Battle backgrounds') + battle_bgs = make_battle_backgrounds(ROM_jp) + perfcount() print('Generating other sprites') self.battle_strips = make_character_battle_sprites(ROM_en) status_strips = make_character_status_sprites(ROM_en) @@ -263,9 +278,13 @@ class FF5Reader(QMainWindow): sprites_tab.addTab(make_pixmap_table(glyph_sprites_jp_small, scale=4), 'Glyphs (JP)') sprites_tab.addTab(make_pixmap_table(glyph_sprites_jp_large, scale=2), 'Glyphs (Large JP)') sprites_tab.addTab(make_pixmap_table(glyph_sprites_kanji, scale=2), 'Glyphs (Kanji)') - sprites_tab.addTab(make_pixmap_table(worldmap_tiles, cols=16, scale=4), 'Worldmap Tiles') + sprites_tab.addTab(make_pixmap_table(worldmap_subtiles, cols=16, scale=4), 'Worldmap Subtiles') + sprites_tab.addTab(make_pixmap_table(world_tiles_pixmaps[0], cols=16, scale=4), 'World 1 Tiles') + sprites_tab.addTab(make_pixmap_table(world_tiles_pixmaps[1], cols=16, scale=4), 'World 2 Tiles') + sprites_tab.addTab(make_pixmap_table(world_tiles_pixmaps[2], cols=16, scale=4), 'Underwater Tiles') sprites_tab.addTab(make_pixmap_table(worldpixmaps, cols=1, scale=1, large=True), 'Worldmaps') sprites_tab.addTab(make_pixmap_table(fieldmap_tiles, cols=8, scale=2), 'Fieldmap Tiles') + sprites_tab.addTab(make_pixmap_table(battle_bgs, cols=8, scale=2), 'Battle BGs') sprites_tab.addTab(make_pixmap_table(self.battle_strips, cols=22, scale=2), 'Character Battle Sprites') sprites_tab.addTab(make_pixmap_table(status_strips, cols=22, scale=2), 'Status Sprites') sprites_tab.addTab(make_pixmap_table(enemy_sprites_named, cols=32, scale=1), 'Enemy Sprites') @@ -279,6 +298,7 @@ class FF5Reader(QMainWindow): structs_tab.addTab(make_table(zone_headers, zone_data, True), 'Zones') structs_tab.addTab(make_table(tileset_headers, tileset_data, True), 'Tilesets') + structs_tab.addTab(make_table(battle_bg_headers, battle_bg_data, True), 'BattleBGs') structs_tab.addTab(make_table(const.npc_layer_headers, npc_layers, True), 'NPC Layers') structs_tab.addTab(make_table(enemy_sprite_headers, enemy_sprite_data, True), 'Enemy Sprites') @@ -487,6 +507,166 @@ def make_field_map_tile_pixmap(rom, id, st_tiles, st_minitiles): canvas.draw_pixmap(0, 48, st_minitiles[minitile].pixmap(p)) return canvas.pixmap() +def decompress_lzss(rom, start, header=False): + ''' + Algorithm from http://slickproductions.org/slickwiki/index.php/Noisecross:Final_Fantasy_V_Compression + ''' + uncompressed_length = indirect(rom, start) + ptr = start+2 + output = [] + buffer = [0 for i in range(0x800)] + buffer_p = 0x07DE + while len(output) < uncompressed_length: + bitmap_byte = rom[ptr] + ptr += 1 + for i in range(8): + bit = (bitmap_byte >> i) & 1 + if bit: + b = rom[ptr] + ptr += 1 + output.append(b) + buffer[buffer_p] = b + buffer_p = (buffer_p+1) % 0x800 + else: + b1 = rom[ptr] + b2 = rom[ptr+1] + ptr += 2 + offset = b1|((b2 & 0xE0)<<3) + length = b2 & 0x1F + for j in range(length+3): + b = buffer[offset] + output.append(b) + buffer[buffer_p] = b + buffer_p = (buffer_p+1) % 0x800 + offset = (offset+1) % 0x800 + return bytes(output[:uncompressed_length]) + +def decompress_battle_terrain(rom, address): + ''' + Decompresses the tilemap for a battle background. + ''' + length = 0x500 + output = [0 for i in range(length)] + o1 = [] + ptr = address + while len(o1) < length//2: + a = rom[ptr] + ptr += 1 + if a != 0xFF: + o1.append(a) + else: + repeat = rom[ptr] + ptr += 1 + if repeat & 0x80: # Repeat 2 tiles + repeat &= 0x3F + a, b = rom[ptr:ptr+2] + ptr += 2 + o1 += [a, b]*repeat + else: + if repeat & 0x40: + pass # TODO: trace this out + else: # Incremental repeat + repeat &= 0x3F + a,inc = rom[ptr:ptr+2] + ptr += 2 + o1 += [a+(i*inc) for i in range(repeat)] + o2 = [4*(1+(i>>7)) for i in o1] + output[::2] = [i|0x80 for i in o1[:length//2]] + output[1::2] = [i&0xDF for i in o2[:length//2]] + return bytes(output) + +def apply_battle_terrain_flips(rom, id, battle_terrain): + if id==0xFF: + return battle_terrain + ptr = indirect(rom, 0x14C736+(id*2))+0x140000 + length = len(battle_terrain)//2 + output = list(battle_terrain) + buffer = [] + + while len(buffer) < length: + a = rom[ptr] + ptr += 1 + if a == 0x00: + skip = rom[ptr] + ptr += 1 + buffer += [0]*skip*4 + else: + for b in reversed(range(0, 8, 2)): + buffer.append((a>>b)&0x03) + + for i in range(len(battle_terrain)//2): + output[i*2+1] |= (buffer[i] << 6) + return bytes(output) + +def make_tilemap_pixmap(tilemap, tiles, palettes, tile_adjust=0): + ''' + Battle bg is 64x64 map size, 8x8 tile size + 4bpp tiles + ''' + canvas = Canvas(64, 64) + for i in range(len(tilemap)//2): + a, b = tilemap[i*2:(i+1)*2] + tile_index = a|((b & 0x02) << 8) + p = (b & 0x1C) >> 2 + priority = (b & 0x20) >> 5 + h_flip = (b & 0x40) >> 6 + v_flip = (b & 0x80) >> 7 + + x = (i % 32) + 32*((i//1024) % 2) + y = (i //32) - 32*((i//1024) % 2) + try: + palette = palettes[p] + tile = tiles[(tile_index+tile_adjust)%0x80] + tile.setColorTable(palette) + tile_px = QPixmap.fromImage(tile) + canvas.draw_pixmap(x, y, tile_px, h_flip, v_flip) + except BaseException as e: + print(e, p, hex(tile_index,2), hex(tile_adjust,2), hex(tile_index+tile_adjust,2)) + return canvas.pixmap(True) + +def make_battle_backgrounds(rom): + ''' + 21 pointers in memory for the compressed data of the tilesets. + Most of these are not unique, and only a subset of the resulting block is used. + The block appears to get DMA'd to 0x0400 in VRAM + + Terrain gets DMA'd to 0x2000 (size 0x500) in VRAM from 0x7f0000 in RAM + ''' + palettes = [generate_palette(rom, 0x14BB31+(i*0x20)) for i in range(84)] + battle_bgs = [] + for i in range(34): + bg = { + 'tiles_id': rom[0x14BA21+(i*8)], + 'pal1_id': rom[0x14BA22+(i*8)], + 'pal2_id': rom[0x14BA23+(i*8)], + 'terrain_id': rom[0x14BA24+(i*8)], + 'terrain_flips_id': rom[0x14BA25+(i*8)], + } + bg['palette'] = [palettes[0], palettes[bg['pal1_id']], palettes[bg['pal2_id']]] + battle_bgs.append(bg) + + tiles_pointer_start = 0x184196 + tiles_RAM_pointer_start = 0x184157 + tiles_pointers = [indirect(rom, tiles_pointer_start+(i*3), length=3)-0xC00000 for i in range(21)] + tiles_raw = [decompress_lzss(rom, p) for p in tiles_pointers] + tiles_skips = [indirect(rom, tiles_RAM_pointer_start+(i*3), length=3)-0x7FC000 for i in range(21)] + tiles = [] + for raw, skip in zip(tiles_raw, tiles_skips): + r = raw[skip:] + tiles.append([create_tile_indexed(r[i*32:(i+1)*32]) for i in range(len(r)//32)]) + + terrain_pointer_start = 0x14C86D + terrain_pointers = [indirect(rom, terrain_pointer_start+(i*2))+0x140000 for i in range(28)] + terrains = [decompress_battle_terrain(rom, p) for p in terrain_pointers] + + pixmaps = [] + for bg in battle_bgs: + terrain = apply_battle_terrain_flips(rom, bg['terrain_flips_id'], terrains[bg['terrain_id']]) + pixmaps.append(make_tilemap_pixmap(terrain, tiles[bg['tiles_id']], bg['palette'])) + #[make_tilemap_pixmap(terrains[5], tiles[2], palettes)] + return pixmaps + + def make_battle_strip(rom, palette_address, tile_address, num_tiles, bpp=4): if isinstance(palette_address, int): palette = generate_palette(rom, palette_address, transparent=True) diff --git a/snestile.py b/snestile.py index eca6a54..b559990 100644 --- a/snestile.py +++ b/snestile.py @@ -210,8 +210,16 @@ class Canvas: def __del__(self): del self.painter - def draw_pixmap(self, col, row, pixmap): - self.painter.drawPixmap(col*8, row*8, pixmap) + def draw_pixmap(self, col, row, pixmap, h_flip=False, v_flip=False): + if h_flip or v_flip: + return + h_s = -1 if h_flip else 1 + v_s = -1 if v_flip else 1 + x = (col+h_flip)*8*h_s + y = (row+v_flip)*8*v_s + self.painter.scale(h_s, v_s) + self.painter.drawPixmap(x, y, pixmap) + self.painter.scale(h_s, v_s) # Invert it again to restore it to normal if col > self.max_x: self.max_x = col if row > self.max_y: