From 7fe8fb73518a325a8e2cf961dffc954524c319df Mon Sep 17 00:00:00 2001 From: Luke Hubmayer-Werner Date: Tue, 27 Mar 2018 01:11:40 +1030 Subject: [PATCH] Animated Battle BGs --- ff5reader.py | 36 +++++------ includes/qthelpers.py | 38 +++++++++--- includes/snes.py | 138 ++++++++++++++++++++++++++++++++++-------- includes/snestile.py | 10 ++- 4 files changed, 168 insertions(+), 54 deletions(-) diff --git a/ff5reader.py b/ff5reader.py index 251e706..9f31aa5 100755 --- a/ff5reader.py +++ b/ff5reader.py @@ -110,9 +110,6 @@ with open(filename_jp_ff6, 'rb') as file: ROM_FF6jp = file.read() print(len(ROM_FF6jp), filename_jp_ff6) -stringlist_headers = ['Address', 'ID', 'Name'] -imglist_headers = stringlist_headers + ['Img', 'Name JP', 'Img JP'] - class FF5Reader(QMainWindow): ''' @@ -120,7 +117,6 @@ class FF5Reader(QMainWindow): ''' def __init__(self): QMainWindow.__init__(self, None) - global glyph_sprites_en_large, glyph_sprites_en_small, glyph_sprites_jp_small, glyph_sprites_jp_large, glyph_sprites_kanji, glyph_sprites_jp_dialogue perfcount() print('Generating Glyphs') self.glyph_sprites = { @@ -133,6 +129,9 @@ class FF5Reader(QMainWindow): make_string_img_list = functools.partial(_make_string_img_list, **self.glyph_sprites) perfcount() + stringlist_headers = ['Address', 'ID', 'Name'] + imglist_headers = stringlist_headers + ['Img', 'Name JP', 'Img JP'] + print('Generating Strings') zone_names = make_string_img_list(0x107000, 2, 0x100, start_str=0x270000, start_jp_str=0x107200, indirect=True, large=True) items = make_string_img_list(0x111380, 9, 256) @@ -180,14 +179,14 @@ class FF5Reader(QMainWindow): 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)] - 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), - ('AnimationID', 1, None), - ('PaletteCycleID',1, None),] + battle_bg_structure = [('Tileset', 1, None), + ('Palette 1', 1, None), + ('Palette 2', 1, None), + ('Tilemap', 1, None), + ('TilemapFlip', 1, None), + (hex(5, 1), 1, None), + ('Animation', 1, None), + ('PaletteCycle',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)] @@ -223,9 +222,7 @@ class FF5Reader(QMainWindow): perfcount() print('Generating map tiles') - worldmap_palettes = [generate_palette(ROM_jp, 0x0FFCC0+(i*0x100), length=0x160, transparent=True) for i in range(3)] - 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 = [] @@ -267,6 +264,7 @@ class FF5Reader(QMainWindow): perfcount() + print('Creating Qt Widgets') self.gamewidget = QTabWidget() self.ff4widget = QTabWidget() self.ff5widget = QTabWidget() @@ -311,8 +309,8 @@ class FF5Reader(QMainWindow): structs_tab.addTab(make_table(enemy_sprite_headers, enemy_sprite_data, True), 'Enemy Sprites') strings_tab.addTab(make_table(imglist_headers, items, row_labels=False), 'Items') - strings_tab.addTab(make_table(imglist_headers, magics, row_labels=False), 'Magics') - strings_tab.addTab(make_table(imglist_headers, more_magics, row_labels=False), 'More Magics') + strings_tab.addTab(make_table(imglist_headers, magics+more_magics, row_labels=False), 'Magics') + #strings_tab.addTab(make_table(imglist_headers, more_magics, row_labels=False), 'More Magics') strings_tab.addTab(make_table(imglist_headers, enemy_names, row_labels=False), 'Enemy Names') strings_tab.addTab(make_table(imglist_headers, character_names, row_labels=False), 'Character Names') strings_tab.addTab(make_table(imglist_headers, job_names, row_labels=False), 'Job Names') @@ -336,6 +334,7 @@ class FF5Reader(QMainWindow): self.main_widget.setMinimumSize(800,600) self.setCentralWidget(self.main_widget) self.show() + perfcount() def _string_decode(self): string = ''.join(self.decoder_input.text().split()) @@ -349,7 +348,10 @@ class FF5Reader(QMainWindow): self.decoder_layout.addWidget(img) self.decoder_input.setText('') - +''' +The painting logic here needs to be moved into includes.snestile at some point. +Once that is done, these functions will be moved to includes.snes which should not have qt dependencies. +''' def make_string_img_small(bytestring, glyphs, jp=False): ''' JP version is not as fancy as this with dakuten, it just puts them on the row above and clips. diff --git a/includes/qthelpers.py b/includes/qthelpers.py index e72d96e..962d302 100644 --- a/includes/qthelpers.py +++ b/includes/qthelpers.py @@ -82,6 +82,31 @@ if not monofont.fixedPitch(): monofont.setFamily('Monospace') +class Label(QLabel): + def __init__(self, *kwargs): + super().__init__(*kwargs) + self.pixmaps = [] + self.pixmap_index = 0 + self.timer = QtCore.QTimer() + self.timer.timeout.connect(self._cycle_pixmap) + + def setContent(self, content, scale=1, strip=True): + if isinstance(content, QPixmap): + self.setPixmap(content.scaled(content.size() * scale)) + elif isinstance(content, list) and isinstance(content[0], QPixmap): + self.pixmaps = [c.scaled(c.size() * scale) for c in content[:-1]] + self.setPixmap(self.pixmaps[0]) + self.timer.start(content[-1]*1000/60) + else: + if strip: + content = content.strip() + self.setText(content) + + def _cycle_pixmap(self): + self.pixmap_index = (self.pixmap_index+1)%len(self.pixmaps) + self.setPixmap(self.pixmaps[self.pixmap_index]) + + def table_size_to_contents(table): # Stupid hack to get table to size correctly table.hide() @@ -144,9 +169,9 @@ def make_pixmap_table(items, cols=16, scale=4, large=False): for i, item in enumerate(items): if isinstance(item, QWidget): table.setCellWidget(i // cols, i % cols, item) - elif isinstance(item, QPixmap): - lab = QLabel() - lab.setPixmap(item.scaled(item.size() * scale)) + else: + lab = Label() + lab.setContent(item, scale=scale) lab.setAlignment(QtCore.Qt.AlignCenter) table.setCellWidget(i // cols, i % cols, lab) table_size_to_contents(table) @@ -161,11 +186,8 @@ def stack_labels(*items): l.setSpacing(0) l.setContentsMargins(0, 0, 0, 0) for item in items: - lab = QLabel() - if isinstance(item, QPixmap): - lab.setPixmap(item) - else: - lab.setText(item.strip()) + lab = Label() + lab.setContent(item) lab.setAlignment(QtCore.Qt.AlignCenter) lab.setMargin(0) l.addWidget(lab) diff --git a/includes/snes.py b/includes/snes.py index fbaeee7..c0bfad0 100644 --- a/includes/snes.py +++ b/includes/snes.py @@ -214,7 +214,7 @@ 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_battle_terrain(rom, address): +def decompress_battle_tilemap(rom, address): ''' Decompresses the tilemap for a battle background. Battle BGs use a type of RLE with 2byte repeat and 1byte incremental repeat. @@ -248,7 +248,7 @@ def decompress_battle_terrain(rom, address): output[1::2] = [i&0xDF for i in o2[:length//2]] return bytes(output) -def apply_battle_terrain_flips(rom, id, battle_terrain): +def apply_battle_tilemap_flips(rom, id, battle_terrain): if id==0xFF: return battle_terrain ptr = indirect(rom, 0x14C736+(id*2))+0x140000 @@ -271,7 +271,30 @@ def apply_battle_terrain_flips(rom, id, battle_terrain): output[i*2+1] |= (buffer[i] << 6) return bytes(output) -def make_tilemap_pixmap(tilemap, tiles, palettes, tile_adjust=0): +def make_tilemap_canvas(tilemap, tiles, tile_adjust=0, pal_adjust=-1): + ''' + Battle bg is 64x64 map size, 8x8 tile size + 4bpp tiles + ''' + canvas = Canvas_Indexed(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: + tile = tiles[(tile_index+tile_adjust)%0x80] + canvas.draw_tile(x, y, tile, h_flip, v_flip, p+pal_adjust) + except BaseException as e: + print(e, p, hex(tile_index,2), hex(tile_adjust,2), hex(tile_index+tile_adjust,2)) + return canvas + +def make_tilemap_pixmap(tilemap, tiles, palettes, tile_adjust=0, pal_adjust=-1): ''' Battle bg is 64x64 map size, 8x8 tile size 4bpp tiles @@ -288,7 +311,7 @@ def make_tilemap_pixmap(tilemap, tiles, palettes, tile_adjust=0): x = (i % 32) + 32*((i//1024) % 2) y = (i //32) - 32*((i//1024) % 2) try: - palette = palettes[p] + palette = palettes[p+pal_adjust] tile = tiles[(tile_index+tile_adjust)%0x80] tile.setColorTable(palette) tile_px = QPixmap.fromImage(tile) @@ -301,45 +324,108 @@ 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)], + 'tileset_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)], + 'tilemap_id': rom[0x14BA24+(i*8)], + 'tilemap_flips_id': rom[0x14BA25+(i*8)], + 'tilecycle_id': rom[0x14BA27+(i*8)], + 'palcycle_id': rom[0x14BA28+(i*8)], } - bg['palette'] = [palettes[0], palettes[bg['pal1_id']], palettes[bg['pal2_id']]] + bg['palette'] = 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): + tileset_pointer_start = 0x184196 + tileset_RAM_pointer_start = 0x184157 + tileset_pointers = [indirect(rom, tileset_pointer_start+(i*3), length=3)-0xC00000 for i in range(21)] + tileset_raw = [decompress_lzss(rom, p) for p in tileset_pointers] + tileset_skips = [indirect(rom, tileset_RAM_pointer_start+(i*3), length=3)-0x7FC000 for i in range(21)] + tileset = [] + for raw, skip in zip(tileset_raw, tileset_skips): r = raw[skip:] - tiles.append([create_tile_indexed(r[i*32:(i+1)*32]) for i in range(len(r)//32)]) + tileset.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] + tilemap_pointer_start = 0x14C86D + tilemap_pointers = [indirect(rom, tilemap_pointer_start+(i*2))+0x140000 for i in range(28)] + tilemaps = [decompress_battle_tilemap(rom, p) for p in tilemap_pointers] + animation_ptr_start = 0x14C5B1 + animation_ptrs = [indirect(rom, animation_ptr_start+(i*2))+0x140000 for i in range(8)] + animations = [] + for ptr in animation_ptrs: + a = [] + for i in range(ptr, ptr+200): + b = rom[i] + if b == 0xFF: + break + a.append(b) + a = [(i, j) for i, j in zip(a[0::2], a[1::2])] + animations.append(a) + animation_time = 15 # Frames before changing + + pal_cycle_ptr_start = 0x14C6CD + pal_cycle_ptrs = [indirect(rom, pal_cycle_ptr_start+(i*2))+0x140000 for i in range(3)] + pal_cycles = [] + for ptr in pal_cycle_ptrs: + a = [] + for i in range(ptr, ptr+100): + b = rom[i] + if b == 0xFF: + break + a.append(b) + pal_cycles.append(a) + + def make_pals(bg): + p_cycle = pal_cycles[bg['palcycle_id']] + p1 = bg['pal1_id'] + p2 = bg['pal2_id'] + pals = [] + for p in p_cycle: + if p & 0x80: + p2 = min(p & 0x7F, len(palettes)-1) + else: + p1 = min(p, len(palettes)-1) + pals.append(palettes[p1] + palettes[p2]) + return pals + + canvases = [] 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)] + tilemap = apply_battle_tilemap_flips(rom, bg['tilemap_flips_id'], tilemaps[bg['tilemap_id']]) + if bg['tilecycle_id'] > 0: + tss = [[t for t in tileset[bg['tileset_id']]] for i in range(4)] + for i, tile2 in animations[bg['tilecycle_id']]: + frame = i >> 6 + tile = i & 0x3F + tss[frame][tile] = tileset[bg['tileset_id']][tile2] + canvases.append([make_tilemap_canvas(tilemap, ts) for ts in tss]) + if bg['palcycle_id'] < 3: + pals = make_pals(bg) + pl = len(pals) + cl = (animation_time*4) + px = [canvases[-1][0].pixmap(pals[0], True)] + i = 1 + while (i%pl != 0) or (i%cl != 0): + px.append(canvases[-1][(i//animation_time)%4].pixmap(pals[i%pl], True)) + i += 1 + pixmaps.append(px + [1]) + else: + pixmaps.append([c.pixmap(bg['palette'], True) for c in canvases[-1]]+[animation_time]) + else: + canvases.append(make_tilemap_canvas(tilemap, tileset[bg['tileset_id']])) + if bg['palcycle_id'] < 3: + pals = make_pals(bg) + pixmaps.append([canvases[-1].pixmap(p, True) for p in pals]+[1]) + else: + pixmaps.append(canvases[-1].pixmap(bg['palette'], True)) return pixmaps -def get_zone_tiles_start(rom, id): +def get_zone_tileset_start(rom, id): i1 = indirect(rom, 0x0E59C0+(id*2))+7 i2 = indirect(rom, 0x0E59C2+i1, 1) # There is a divergent path here based on the value. Other things appear to be affected by this. diff --git a/includes/snestile.py b/includes/snestile.py index 3dc77ff..661bbad 100644 --- a/includes/snestile.py +++ b/includes/snestile.py @@ -25,14 +25,14 @@ skip_pyqt5 = "PYQT4" in os.environ if not skip_pyqt5: try: from PyQt5 import QtGui - from PyQt5.QtGui import QImage, QPixmap, QColor, QPainter + from PyQt5.QtGui import QImage, QPixmap, QColor, QPainter, QTransform pyqt_version = 5 except ImportError: print("Missing PyQt5, trying PyQt4...") if pyqt_version == 0: try: from PyQt4 import QtGui - from PyQt4.QtGui import QImage, QPixmap, QColor, QPainter + from PyQt4.QtGui import QImage, QPixmap, QColor, QPainter, QTransform pyqt_version = 4 except ImportError: print("Missing PyQt4 dependencies") @@ -253,9 +253,13 @@ class Canvas_Indexed: self.max_col = 1 self.max_row = 1 - def draw_tile(self, col, row, image): + def draw_tile(self, col, row, image, h_flip=False, v_flip=False, palette=0): + image = image.mirrored(h_flip, v_flip) imgbits = image.bits() imgbits.setsize(image.byteCount()) + if palette: + p = palette<<4 + imgbits[:] = bytes([int(i[0])|p for i in imgbits]) x = col*self.tilesize y = row*self.tilesize start = x + y*self.width